Skip to content

Add Forkana Docker deployment configuration#128

Draft
pedrogaudencio wants to merge 17 commits intomasterfrom
docker-deploy
Draft

Add Forkana Docker deployment configuration#128
pedrogaudencio wants to merge 17 commits intomasterfrom
docker-deploy

Conversation

@pedrogaudencio
Copy link
Collaborator

@pedrogaudencio pedrogaudencio commented Feb 2, 2026

  • create Dockerfile
  • create docker compose setup
  • test setup
  • add deployment guide
  • add local registry

Closes /issues/111

Co-authored by: Claude Opus 4.6, Sonnet 4.6 and GLM-5


Open with Devin

@pedrogaudencio pedrogaudencio self-assigned this Feb 2, 2026
@pedrogaudencio pedrogaudencio added the enhancement New feature or request label Feb 2, 2026
@pedrogaudencio pedrogaudencio changed the title Docker deploy Add Forkana Docker deployment configuration Feb 2, 2026
@pedrogaudencio pedrogaudencio marked this pull request as ready for review February 24, 2026 14:29
Copilot AI review requested due to automatic review settings February 24, 2026 14:29
devin-ai-integration[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@pedrogaudencio
Copy link
Collaborator Author

pedrogaudencio commented Feb 24, 2026

/crush_fast


AI review started.

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5
Click to expand review

Code Review: Forkana Docker Deployment Configuration

Issue 1: Overly Permissive Trusted Proxies Configuration 🔴

  • Addressed
  • Dismissed

Location: docker/forkana/etc/templates/app.ini:69

REVERSE_PROXY_TRUSTED_PROXIES = *

Problem: Setting REVERSE_PROXY_TRUSTED_PROXIES = * trusts ALL proxies to set X-Forwarded-For headers. This allows any client to spoof their IP address by setting these headers, bypassing IP-based rate limiting and audit logging.

Recommendation: Restrict to known proxy networks. Since nginx is on the same host:

REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.0/8,::1/128,172.16.0.0/12,10.0.0.0/8

Or if the nginx proxy is in a Docker network, use the appropriate CIDR range.


Issue 2: Contradictory SSH Configuration 🔴

  • Addressed
  • Dismissed

Location: docker/forkana/etc/templates/app.ini:31-33

DISABLE_SSH      = $DISABLE_SSH
; In rootless container only internal ssh server is supported
START_SSH_SERVER = true

Problem: The template always sets START_SSH_SERVER = true regardless of DISABLE_SSH value. When DISABLE_SSH=true (the default per dev.yml:78), starting an SSH server is wasteful and potentially confusing. The original rootless template has the same issue, but the Forkana template should be improved since SSH is explicitly disabled.

Recommendation: Make START_SSH_SERVER configurable:

DISABLE_SSH      = $DISABLE_SSH
START_SSH_SERVER = $START_SSH_SERVER

And in docker/forkana/usr/local/bin/docker-setup.sh:47-48, add:

DISABLE_SSH=${DISABLE_SSH:-"true"} \
START_SSH_SERVER=${START_SSH_SERVER:-"false"} \

Issue 3: Unnecessary Runtime Dependency on Registry 🟡

  • Addressed
  • Dismissed

Location: docker/forkana/dev.yml:53-54

depends_on:
  postgres:
    condition: service_healthy
  registry:
    condition: service_started

Problem: The forkana service depends on registry at runtime, but the registry is only needed during image build/push. The forkana container uses a digest-pinned image from compose.override.yml and doesn't communicate with the registry at runtime. This creates an unnecessary startup dependency.

Recommendation: Remove the registry dependency:

depends_on:
  postgres:
    condition: service_healthy

Issue 4: Missing Modern Security Headers in nginx 🟡

  • Addressed
  • Dismissed

Location: docker/forkana/nginx.conf:23-28

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Problem: Missing Content-Security-Policy and Permissions-Policy headers which provide important XSS and feature access protections. Also, X-XSS-Protection is deprecated in modern browsers (replaced by CSP).

Recommendation: Add modern security headers:

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Issue 5: Missing INTERNAL_TOKEN Configuration 🟡

  • Addressed
  • Dismissed

Location: docker/forkana/dev.yml and docker/forkana/etc/templates/app.ini

Problem: INTERNAL_TOKEN is required for internal API communication between Gitea processes. Without it being explicitly configured, the system auto-generates one on first run. If the container is recreated or the config volume is lost, a new token is generated, potentially breaking existing internal integrations.

Recommendation: Add to .env.example:

# Internal token for API communication (generate with: gitea generate secret INTERNAL_TOKEN)
FORKANA_INTERNAL_TOKEN=your-internal-token-here

Add to dev.yml environment:

GITEA__security__INTERNAL_TOKEN: ${FORKANA_INTERNAL_TOKEN}

Issue 6: Database Password Logging Risk in Template 🟡

  • Addressed
  • Dismissed

Location: docker/forkana/etc/templates/app.ini:45

PASSWD   = $DB_PASSWD

Problem: The database password is written to app.ini in plaintext. While Gitea does this by default, it's a security consideration. The file is stored on the config volume which should have restricted permissions.

Recommendation: This is consistent with upstream Gitea behavior, but consider documenting that the config volume (~/forkana/config) must have restricted permissions. The docker-setup.sh already sets chmod 0700 on the directory, which is good. Consider adding a note in DEPLOYMENT_GUIDE.md about this.


Issue 7: Potentially Fragile Array Handling in gitea Wrapper ⚪️

  • Addressed
  • Dismissed

Location: docker/forkana/usr/local/bin/gitea:34-40

if [ -z "$APP_INI_SET" ]; then
	CONF_ARG=("-c" "${GITEA_APP_INI:-$APP_INI}")
fi


# Provide docker defaults
GITEA_WORK_DIR="${GITEA_WORK_DIR:-$WORK_DIR}" exec -a "$0" "$GITEA" "${CONF_ARG[@]}" "$@"

Problem: If APP_INI_SET is set (user passed -c or --config), CONF_ARG is never declared, making "${CONF_ARG[@]}" expand to nothing. This works in bash (unset arrays expand to empty), but is fragile and could cause issues with set -u if ever enabled.

Recommendation: Initialize the array at declaration:

CONF_ARG=()
APP_INI_SET=""
for i in "$@"; do
    case "$i" in
    "-c")
        APP_INI_SET=1
        ;;
    # ... rest of cases
    esac
done

if [ -z "$APP_INI_SET" ]; then
    CONF_ARG=("-c" "${GITEA_APP_INI:-$APP_INI}")
fi

Issue 8: Hardcoded Domain in nginx.conf ⚪️

  • Addressed
  • Dismissed

Location: docker/forkana/nginx.conf:4,15,18-19

server_name dev.forkana.org;
...
ssl_certificate /etc/letsencrypt/live/dev.forkana.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev.forkana.org/privkey.pem;

Problem: The domain dev.forkana.org is hardcoded. While the DEPLOYMENT_GUIDE.md mentions using sed to replace it, this is a potential source of deployment errors.

Recommendation: The DEPLOYMENT_GUIDE already documents the sed replacement command. Consider adding a placeholder that makes it more obvious:

server_name YOUR_DOMAIN_HERE;  # Replace with your actual domain

Or create an nginx.conf.example alongside the actual config.


Issue 9: Missing OAuth2 JWT_SECRET Configuration ⚪️

  • Addressed
  • Dismissed

Location: docker/forkana/dev.yml and related configuration

Problem: OAuth2 functionality requires a JWT_SECRET (even if OAuth2 isn't explicitly used, Gitea generates one). Without explicit configuration, it's auto-generated. While the DEPLOYMENT_GUIDE.md mentions it in the "Additional Configuration" section, it's not part of the core setup.

Recommendation: Add to .env.example and dev.yml similar to FORKANA_SECRET_KEY, or note in DEPLOYMENT_GUIDE that these are auto-generated on first run.


Summary

Rating Count Description
🔴 2 High importance security/logic issues
🟡 4 Medium importance issues
⚪️ 3 Lower importance/minor issues

The deployment configuration is generally well-structured with good practices like:

  • Digest-pinned registry image
  • UID 1000 for rootless operation
  • SSH forced-command restrictions in deployment
  • Proper volume permission handling
  • IPv4-only curl for health checks

The most critical issues to address are Issue 1 (trusted proxies) and Issue 2 (SSH configuration contradiction).


Review generated using glm-5 via Z.AI. Comment /review to re-run.

…r, systemd WorkingDirectory

* add  to Dockerfile custom-defaults loop so Forkana CSS/images are baked in
* change nginx.conf placeholder from dev.forkana.org to dev.forkana.example (RFC 2606),
  making the deployment guide's sed command match correctly
* fix systemd WorkingDirectory: ~/... → %h/... (systemd does not expand tilde)
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

@pedrogaudencio pedrogaudencio marked this pull request as draft February 24, 2026 17:40
* add FORKANA_INTERNAL_TOKEN and FORKANA_JWT_SECRET to .env.example and wire them via GITEA__ env overrides in dev.yml
* initialize CONF_ARG=() before arg-parsing loop in gitea wrapper to prevent errors under set -u
@pedrogaudencio
Copy link
Collaborator Author

pedrogaudencio commented Mar 10, 2026

/crush_fast


AI review started.

@github-actions
Copy link

github-actions bot commented Mar 10, 2026

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5
Click to expand review

I'll review these Docker deployment changes thoroughly. Let me first examine some existing files in the codebase to understand the context and check for any potential conflicts.Now let me check if there are any existing deployment-related scripts that might conflict:Now I have a comprehensive understanding of the codebase. Let me review the changes in detail:


Code Review: Forkana Docker Deployment Configuration

Issue 1: Potential postgres volume path issue when HOME is not set

  • Addressed
  • Dismissed

Rating: 🔴 High importance

File: docker/forkana/dev.yml:55

volumes:
  - ${HOME}/forkana/postgres:/var/lib/postgresql/data

The postgres volume uses ${HOME} which will fail if the HOME environment variable is not set in the shell context where docker compose runs. While this works when run as the deploy user (where HOME is set), it could cause confusing errors if run in other contexts.

Suggestion: Use ~ which Docker Compose expands, or document that HOME must be set. Alternatively, consider using a named volume for postgres data for consistency with how registry-data is handled:

volumes:
  - forkana-postgres:/var/lib/postgresql/data

# Under volumes section:
volumes:
  forkana-postgres:

This also improves portability and avoids issues with permission mismatches.


Issue 2: Missing GITEA_APP_INI environment variable in compose file

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/dev.yml (and docker/forkana/usr/local/bin/docker-entrypoint.sh:40)

The entrypoint script references GITEA_APP_INI:

exec /usr/local/bin/gitea -c ${GITEA_APP_INI} web

The environment variable is set in the Dockerfile (ENV GITEA_APP_INI=/etc/gitea/app.ini), but if someone runs the container with a different configuration file location, they would need to override both GITEA_APP_INI and potentially pass -c explicitly. This is not a bug per se, but the documentation could clarify this relationship.


Issue 3: Inconsistent variable expansion in envsubst - DOMAIN not defaulted consistently

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/usr/local/bin/docker-setup.sh:50-51

ROOT_URL=${ROOT_URL:-"http://${DOMAIN:-localhost}:${HTTP_PORT:-3000}/"} \

The ROOT_URL default uses ${DOMAIN:-localhost} but this happens during the envsubst execution. However, DOMAIN is set as a separate variable on line 47:

DOMAIN=${DOMAIN:-"localhost"} \

The ordering is correct (DOMAIN is set before ROOT_URL in the same multi-line command), but this pattern is fragile. If someone accidentally reorders the lines, ROOT_URL would use the environment's DOMAIN value, not the default. Consider using explicit values:

ROOT_URL=${ROOT_URL:-"http://localhost:3000/"} \

Or document the ordering requirement clearly.


Issue 4: Comment typo in docker-entrypoint.sh

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance

File: docker/forkana/usr/local/bin/docker-entrypoint.sh:4

# Protect against buggy runc in docker <20.10.6 causing problems with Alpine >= 3.14

The original Gitea comment had a typo ("problems in with") that was fixed in the Forkana version. This is actually correct in the PR. No change needed.


Issue 5: Registry image digest pinning - verify SHA256

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance (verification)

File: docker/forkana/dev.yml:6-7

image: registry:3.0.0@sha256:6c5666b861f3505b116bb9aa9b25175e71210414bd010d92035ff64018f9457e

This is good practice (digest pinning). I cannot verify the digest matches the official registry:3.0.0 image without network access. Recommend verifying against Docker Hub or the official registry repository.


Issue 6: DB_PASSWD empty default in setup script

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/usr/local/bin/docker-setup.sh:53

DB_PASSWD=${DB_PASSWD:-""} \

The empty default for DB_PASSWD means if someone forgets to set this, the container will still start but database connections will fail. Consider adding validation:

DB_PASSWD=${DB_PASSWD:-""}
if [ "$DB_TYPE" = "postgres" ] && [ -z "$DB_PASSWD" ]; then
    echo "WARNING: DB_PASSWD is empty but DB_TYPE is postgres"
fi

Or require it to be set:

if [ "$DB_TYPE" = "postgres" ] && [ -z "$DB_PASSWD" ]; then
    echo "ERROR: DB_PASSWD must be set when using PostgreSQL"
    exit 1
fi

Issue 7: nginx.conf missing Content-Security-Policy header

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/nginx.conf:21-25

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Consider adding a Content-Security-Policy header for additional protection against XSS attacks. Since Gitea/Forkana is a complex application with inline scripts, a restrictive CSP might break functionality, but at minimum consider:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always;

Test thoroughly as this may break certain features.


Issue 8: Deploy script duplicate code between Debian and Fedora versions

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance (maintainability)

Files: docker/forkana/deploy_debian.sh and docker/forkana/deploy_fedora.sh

The two deploy scripts share approximately 90% of their code. Only the package installation section (step 0 in Fedora, absent in Debian) differs. Consider extracting common logic to a shared library:

# deploy_common.sh - sourced by both deploy scripts
log() { printf '[deploy %s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
die() { log "FATAL: $*"; exit 1; }
# ... rest of shared functions

Then each OS-specific script sources it and adds only its package-specific logic. However, this adds complexity and the current approach works fine for two scripts.


Issue 9: FORKANA_INTERNAL_TOKEN and FORKANA_JWT_SECRET generation commands not provided

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/.env.example:11-15

# Internal token for API communication between Gitea processes
# (generate with: gitea generate secret INTERNAL_TOKEN)
FORKANA_INTERNAL_TOKEN=

# OAuth2 JWT secret for token signing
# (generate with: gitea generate secret JWT_SECRET)
FORKANA_JWT_SECRET=

The comments reference gitea generate secret but this requires the gitea binary which may not be available on the server before deployment. The DEPLOYMENT_GUIDE.md provides alternative generation methods using /dev/urandom | base64, but the .env.example file has empty values which could lead to copy-paste errors.

Consider providing fallback generation commands in the comments:

# Internal token for API communication between Gitea processes
# Generate with: head -c 32 /dev/urandom | base64
FORKANA_INTERNAL_TOKEN=

Issue 10: Potential race condition in health check loop

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance

File: docker/forkana/deploy_debian.sh:226-238 (same in deploy_fedora.sh)

for i in $(seq 1 30); do
  if curl -sf http://127.0.0.1:3000/api/healthz > /dev/null 2>&1; then
    log "Service is healthy!"
    log "Deployment of ${COMMIT_SHA} complete."
    exit 0
  fi
  log "  attempt ${i}/30 — not ready yet"
  sleep 5
done

This is fine for the intended use case. The 150-second timeout should be sufficient for most deployments. No change needed.


Issue 11: PostgreSQL password exposure in environment

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance (informational)

File: docker/forkana/dev.yml:39

DB_PASSWD: ${POSTGRES_PASSWORD}

The PostgreSQL password is passed as an environment variable which is visible in docker inspect and process listings. This is standard Docker practice, but worth noting for security-conscious deployments. Docker secrets or file-based credential loading would be more secure for production use.


Issue 12: dev.yml missing explicit container_name for postgres

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance

File: docker/forkana/dev.yml:19

postgres:
  image: postgres:14-alpine
  container_name: forkana-postgres

Actually, this is present. No issue.


Issue 13: Missing restart policy for registry could cause orphaned containers

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance

File: docker/forkana/dev.yml:11

restart: unless-stopped

The registry has a proper restart policy. No issue.


Issue 14: DEPLOYMENT_GUIDE.md contains sensitive example values that could be copy-pasted

  • Addressed
  • Dismissed

Rating: 🟡 Medium importance

File: docker/forkana/DEPLOYMENT_GUIDE.md:361-373

The guide provides shell commands to generate the .env file. However, the domain dev.forkana.org is hardcoded:

echo "FORKANA_DOMAIN=dev.forkana.org" >> $DEPLOY_HOME/forkana/compose/.env

Someone might run this verbatim and end up with an incorrect domain. Consider using a placeholder that's more obviously wrong:

echo "FORKANA_DOMAIN=your-domain.example" >> $DEPLOY_HOME/forkana/compose/.env

Issue 15: Potential issue with .env file location in compose

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance (documentation)

File: docker/forkana/DEPLOYMENT_GUIDE.md:360

The guide creates .env at $DEPLOY_HOME/forkana/compose/.env and Docker Compose should auto-load it from the project directory. This is correct when --project-directory is set to the compose directory. The deploy scripts correctly use --project-directory "${COMPOSE_DIR}" so this should work.


Issue 16: Dockerfile missing LABEL for version/version control

  • Addressed
  • Dismissed

Rating: ⚪️ Low importance

File: docker/forkana/Dockerfile:49

LABEL maintainer="Forkana Maintainers <maintainers@forkana.org>"

Consider adding OCI image labels for better image provenance:

LABEL org.opencontainers.image.source="https://github.com/okTurtles/forkana" \
      org.opencontainers.image.description="Forkana - a fork of Gitea for encyclopedia articles" \
      org.opencontainers.image.licenses="MIT"

Summary

Rating Count
🔴 High 1
🟡 Medium 6
⚪️ Low 4

The most important issue is Issue 1 regarding the postgres volume path. The deployment configuration is generally well-structured with good security practices including digest pinning, SSH forced-command restrictions, and proper secret handling documentation. The main areas for improvement are around defensive coding for missing environment variables and some documentation enhancements to prevent copy-paste errors.


Review generated using glm-5 via Z.AI. Comment /crush_fast to re-run.

* use Docker's official repo and DNF5 syntax for Fedora 41+ installs
* add step to verify/fix deploy user home directory ownership (common Fedora issue with --system users)
* replace tilde (~) paths with explicit  from getent passwd and use sudo -Hiu for correct HOME resolution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Dockerfile + anything else to deploy Forkana changes

3 participants