Anchor provides a single Docker image that includes everything you need to run your self-hosted instance. The image uses embedded PostgreSQL by default, making setup as simple as running a single container.
Quick Start
The fastest way to get started is using the pre-built Docker image:
Create docker-compose.yml
Create a docker-compose.yml file with the minimal configuration: services :
anchor :
image : ghcr.io/zhfahim/anchor:latest
container_name : anchor
restart : unless-stopped
ports :
- "3000:3000"
volumes :
- anchor_data:/data
volumes :
anchor_data :
This configuration uses embedded PostgreSQL with auto-generated credentials. Data is persisted in the anchor_data volume.
Start the container
Launch Anchor with Docker Compose: The container will:
Initialize embedded PostgreSQL in /data/postgres
Auto-generate a JWT secret (persisted in /data/.jwt_secret)
Run database migrations
Start the API server (port 3001 internally)
Start the web frontend (port 3000 exposed)
Access your instance
Open your browser and navigate to: You can now create your first account and start using Anchor.
Deployment Options
Option 1: Pre-built Image (Recommended)
Use the official image from GitHub Container Registry:
services :
anchor :
image : ghcr.io/zhfahim/anchor:latest
container_name : anchor
restart : unless-stopped
ports :
- "3000:3000"
environment :
- APP_URL=https://notes.example.com
- JWT_SECRET=your-secret-key-here
volumes :
- anchor_data:/data
volumes :
anchor_data :
Option 2: Build from Source
If you want to build from source or customize the image:
Clone the repository
git clone https://github.com/zhfahim/anchor.git
cd anchor
Build and start
The included docker-compose.yml builds from source:
Architecture
The Anchor Docker image uses a unique single-container architecture:
Base Image : PostgreSQL 18 Alpine with Node.js runtime
Process Management : Supervisord manages multiple services
Services :
Embedded PostgreSQL (optional, port 5432)
NestJS API server (internal port 3001)
Next.js web frontend (exposed port 3000)
Dockerfile Structure
supervisord.conf
FROM postgres:18-alpine AS runner
# Node runtime copied from node:24-alpine
COPY --from=base /usr/local/bin/node /usr/local/bin/node
# Server (NestJS + Prisma)
COPY --from=server_builder /app/server/dist ./server/dist
COPY --from=server_prod_deps /app/server/node_modules ./server/node_modules
# Web (Next.js standalone)
COPY --from=web_builder /app/web/.next/standalone ./web
COPY --from=web_builder /app/web/.next/static ./web/.next/static
Volume Management
Data Directory Structure
The /data volume contains all persistent data:
/data/
├── postgres/ # PostgreSQL database files (if using embedded)
└── .jwt_secret # Auto-generated JWT secret
Always back up the /data volume before upgrading or migrating your instance.
Named Volume (Recommended)
volumes :
- anchor_data:/data
volumes :
anchor_data :
Bind Mount
For easier backups, use a bind mount:
volumes :
- ./anchor-data:/data
Reverse Proxy Setup
Nginx
server {
listen 443 ssl http2;
server_name notes.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1 ;
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection 'upgrade' ;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
proxy_cache_bypass $ http_upgrade ;
}
}
Traefik
services :
anchor :
image : ghcr.io/zhfahim/anchor:latest
environment :
- APP_URL=https://notes.example.com
labels :
- "traefik.enable=true"
- "traefik.http.routers.anchor.rule=Host(`notes.example.com`)"
- "traefik.http.routers.anchor.entrypoints=websecure"
- "traefik.http.routers.anchor.tls.certresolver=myresolver"
- "traefik.http.services.anchor.loadbalancer.server.port=3000"
Caddy
notes.example.com {
reverse_proxy localhost:3000
}
Remember to set APP_URL to your public URL when using a reverse proxy, especially for OIDC authentication.
Health Checks
The Docker image includes a built-in health check:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
CMD curl -f http://localhost:3000/api/health || exit 1
Check container health:
docker ps
# Look for "healthy" status
docker inspect anchor | jq '.[0].State.Health'
Common Issues
Container won’t start
Check logs for errors:
docker compose logs -f anchor
Permission issues with volumes
Ensure the data directory is writable:
sudo chown -R 1000:1000 ./anchor-data
Port conflicts
If port 3000 is already in use, change the mapping:
ports :
- "8080:3000" # Access via http://localhost:8080
Next Steps
Configuration Configure environment variables and settings
Database Options Use external PostgreSQL for production
Updating Keep your instance up to date