Docker Deployment
Forja ships with a multi-stage Dockerfile that builds both the React admin dashboard and the Rust backend into a single, minimal production image.
Quick Start
The fastest way to run Forja in production:
# 1. Generate a .env with secure random secrets
./scripts/forja-init.sh
# 2. Start everything
docker compose -f docker-compose.prod.yml up -d
This starts PostgreSQL, Redis, and Forja with health checks, persistent volumes, and auto-configured database extensions. No source code checkout required beyond the compose file and init script.
Docker Hub
Pre-built images are published to Docker Hub on every push to main. Multi-platform images are available for linux/amd64 and linux/arm64.
docker pull dominikdorfstetter/forja
Images are tagged with:
latest— the most recent build frommain- Git SHA (e.g.
bf3df6d) — for pinning to a specific commit
Prerequisites
- Docker 20.10 or later
- Docker Compose v2 (included with Docker Desktop)
Multi-Stage Build Overview
The Dockerfile uses three stages to keep the final image small:
| Stage | Base Image | Purpose |
|---|---|---|
| admin-build | node:24-alpine | Installs npm dependencies and builds the React admin dashboard |
| backend-build | rust:1.93-bookworm | Compiles the Rust backend in release mode, embedding the admin static files |
| runtime | debian:bookworm-slim | Minimal runtime with only ca-certificates, libssl3, and libpq5 |
The final image contains a single binary (forja), the compiled admin dashboard static files, and the SQLx migration files.
Building the Image
From the repository root:
docker build -t forja .
The first build takes approximately 10-15 minutes due to Rust compilation. Subsequent builds benefit from Docker layer caching.
Build Optimizations
The Dockerfile sets two environment variables to reduce memory usage during Rust compilation:
ENV CARGO_PROFILE_RELEASE_LTO=thin
ENV CARGO_PROFILE_RELEASE_CODEGEN_UNITS=2
These settings prevent out-of-memory errors on machines with limited RAM (e.g., 2 GB CI runners or cloud build environments).
Running with Docker Compose
Production Compose (recommended)
The repository includes docker-compose.prod.yml, a standalone compose file that uses the pre-built Docker Hub image. It does not require the source code -- just the compose file and a .env:
# Generate .env with secure random secrets
./scripts/forja-init.sh
# Start all services
docker compose -f docker-compose.prod.yml up -d
The production compose auto-constructs DATABASE_URL from the PostgreSQL credentials in your .env, so you only need to set POSTGRES_PASSWORD once.
Source Build Compose
If you prefer to build from source, the default docker-compose.yml builds the image locally:
# Set POSTGRES_PASSWORD in .env or export it
docker compose up -d
Database Extensions
The required PostgreSQL extensions are created automatically on first container start:
uuid-ossp-- UUID generationcitext-- case-insensitive text typepg_trgm-- trigram matching for search
The production compose embeds the extension SQL inline using Docker Compose configs. The source build compose mounts backend/scripts/init-extensions.sql instead.
If you are using a managed PostgreSQL service, you must create these extensions manually. See the Railway guide for an example.
Production Environment Variables
At minimum, set the following variables for a production deployment:
| Variable | Value | Description |
|---|---|---|
DATABASE_URL | postgres://user:pass@host:5432/db | PostgreSQL connection string |
REDIS_URL | redis://host:6379 | Redis connection string |
APP__ENVIRONMENT | production | Enables production behavior |
APP__HOST | 0.0.0.0 | Bind to all interfaces |
APP__PORT | 8000 | Application port |
CORS_ALLOWED_ORIGINS | https://yourdomain.com | Allowed CORS origins (comma-separated) |
For the full list of environment variables, see Environment Variables.
Health Checks
The application exposes a health endpoint at /health that returns the status of PostgreSQL and Redis connections:
curl http://localhost:8000/health
{
"status": "healthy",
"postgres": "connected",
"redis": "connected"
}
Use this endpoint in your Docker health check or load balancer configuration:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Verifying the Deployment
After starting the container, verify the following endpoints:
| URL | Expected Result |
|---|---|
http://localhost:8000/health | JSON health status |
http://localhost:8000/api-docs | Swagger UI |
http://localhost:8000/dashboard | Admin dashboard |
Blog Preview (Astro Template)
The Astro blog template at templates/astro-blog/ has its own Dockerfile for building the SSR preview service. The build stage compiles the local libraries (@forjacms/analytics, @forjacms/client, @forjacms/sections) before installing the template dependencies, since their dist/ directories are gitignored.
# Build local libs (dist/ is gitignored, must be built in Docker)
COPY libs/analytics/ /libs/analytics/
WORKDIR /libs/analytics
RUN npm ci && npm run build
COPY libs/client/ /libs/client/
WORKDIR /libs/client
RUN npm ci && npm run build
COPY libs/sections/ /libs/sections/
WORKDIR /libs/sections
RUN npm ci && npm run build
The @forjacms/sections build step is required because the template imports section components and types from it. Without building it first, the Astro build will fail with missing module errors.
The preview service runs on port 4321 and is included in docker-compose.dev.yaml for local development.
Migrations
SQLx database migrations run automatically when the application starts. The migration files are bundled into the Docker image from backend/migrations/. No manual migration step is required.
Updating
To update a running deployment using the Docker Hub image:
docker pull dominikdorfstetter/forja
docker compose up -d
Or if building from source:
git pull
docker build -t forja .
docker compose up -d
Migrations are applied automatically on startup, so schema changes are handled without manual intervention.
Backup and Restore
Database Backup
Create a PostgreSQL dump while the stack is running:
docker compose -f docker-compose.prod.yml exec postgres \
pg_dump -U forja -d forja --format=custom -f /tmp/forja.dump
docker compose -f docker-compose.prod.yml cp postgres:/tmp/forja.dump ./forja-backup.dump
Database Restore
Restore from a backup file:
docker compose -f docker-compose.prod.yml cp ./forja-backup.dump postgres:/tmp/forja.dump
docker compose -f docker-compose.prod.yml exec postgres \
pg_restore -U forja -d forja --clean --if-exists /tmp/forja.dump
Upload Files Backup
The uploads volume stores media files. Back it up with:
docker run --rm \
-v forja_uploads:/data \
-v "$(pwd)":/backup \
alpine tar czf /backup/forja-uploads.tar.gz -C /data .
Restore:
docker run --rm \
-v forja_uploads:/data \
-v "$(pwd)":/backup \
alpine sh -c "cd /data && tar xzf /backup/forja-uploads.tar.gz"
Automated Backups
For scheduled backups, add a cron job on the host:
# Daily database backup at 2:00 AM, keep last 7 days
0 2 * * * cd /path/to/forja && docker compose -f docker-compose.prod.yml exec -T postgres pg_dump -U forja -d forja --format=custom > backups/forja-$(date +\%Y\%m\%d).dump && find backups/ -name "forja-*.dump" -mtime +7 -delete