Skip to content

Docker

Deploy можно. with Docker Compose. One command to get the server, PostgreSQL database, and all dependencies running.

Prerequisites

  • Docker 24+ and Docker Compose v2
  • At least 1 GB of available RAM

Quick Start

Create a docker-compose.yml file and run:

bash
docker compose up -d

The web dashboard will be available at http://localhost:8080.

docker-compose.yml

yaml

services:
  postgres:
    image: postgres:15-alpine
    container_name: mozhno-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: feature_flags
      POSTGRES_USER: flags_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:-flags_password}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U flags_user -d feature_flags"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 512M
    networks:
      - mozhno-net

  mozhno:
    image: ghcr.io/mozhno-dev/mozhno:latest
    container_name: mozhno-server
    restart: unless-stopped
    ports:
      - "8080:8080"
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp:size=128M
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/feature_flags
      SPRING_DATASOURCE_USERNAME: flags_user
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-flags_password}

      JWT_SECRET: ${JWT_SECRET:-}
      JWT_ACCESS_TOKEN_TTL_MINUTES: "15"
      JWT_REFRESH_TOKEN_TTL_DAYS: "30"

      SERVER_PORT: "8080"
      HIKARI_MAX_POOL_SIZE: "30"
      HIKARI_MIN_IDLE: "5"
      CACHE_TTL_MINUTES: "5"

      JAVA_TOOL_OPTIONS: >
        -XX:+UseZGC
        -XX:MaxRAMPercentage=75.0
        -XX:+ExitOnOutOfMemoryError
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2"
        reservations:
          memory: 512M
          cpus: "0.25"
    networks:
      - mozhno-net

volumes:
  pgdata:

networks:
  mozhno-net:
    driver: bridge

Environment Variables

Database

VariableRequiredDefaultDescription
SPRING_DATASOURCE_URLNojdbc:postgresql://localhost:5432/feature_flagsJDBC URL for PostgreSQL
SPRING_DATASOURCE_USERNAMENoflags_userDatabase user
SPRING_DATASOURCE_PASSWORDNoflags_passwordDatabase password
HIKARI_MAX_POOL_SIZENo20Maximum database connections
HIKARI_MIN_IDLENo5Minimum idle database connections

JWT

VariableRequiredDefaultDescription
JWT_SECRETYesHMAC-SHA256 signing secret. Minimum 32 bytes.
JWT_ACCESS_TOKEN_TTL_MINUTESNo15Access token lifetime in minutes
JWT_REFRESH_TOKEN_TTL_DAYSNo30Refresh token lifetime in days

Server

VariableRequiredDefaultDescription
SERVER_PORTNo8080HTTP port

JVM

VariableRequiredDefaultDescription
JAVA_TOOL_OPTIONSNoSee belowJVM arguments

JVM defaults:

  • -XX:+UseZGC — Z Garbage Collector for low-latency pause times
  • -XX:MaxRAMPercentage=75.0 — Max heap at 75% of container memory limit
  • -XX:+ExitOnOutOfMemoryError — Fail fast on OOM instead of hanging
  • -Djava.security.egd=file:/dev/./urandom — Faster random number generation

Optional

VariableRequiredDefaultDescription
LOGGING_LEVEL_DEV_MOZHNONoINFOApplication log level (DEBUG, INFO, WARN, ERROR)
SPRING_FLYWAY_ENABLEDNotrueRun Flyway migrations on startup
CACHE_TTL_MINUTESNo5In-memory cache TTL in minutes
CLIENT_MAX_METRICS_PER_KEYNo1000Max stored metrics per client key
APP_BASE_URLNohttp://localhost:8080Public base URL of the server

Health Checks

The server exposes health endpoints via Spring Boot Actuator:

EndpointPurpose
/actuator/healthOverall health status (DB, disk space)
/actuator/health/livenessApplication liveness — is the JVM running?
/actuator/health/readinessApplication readiness — can it serve traffic?

Docker health check uses /actuator/health to detect unhealthy containers.

Volumes

VolumePurpose
pgdataPostgreSQL data directory. Persists across container restarts.

Resource Limits

ServiceCPU LimitMemory LimitCPU ReservationMemory Reservation
mozhno2 cores2 GB0.25 cores512 MB
postgres512 MB

Adjust these based on your workload. Memory reservations ensure the scheduler places containers on nodes with sufficient resources. CPU reservations control minimum CPU shares.

Network Configuration

All services communicate over the mozhno-net bridge network. PostgreSQL is not exposed to the host — only the Mozhno server port 8080 is published.

For production:

  • Place the stack behind a reverse proxy (nginx, Traefik, Caddy) for TLS termination
  • Use an external PostgreSQL if you already run a managed database
  • Bind the server port to 127.0.0.1 if using a reverse proxy on the same host:
yaml
ports:
  - "127.0.0.1:8080:8080"

Security Considerations

Non-Root User

The container runs as user 1000:1000 (named mozhno), not as root. This limits the impact of a container escape.

Read-Only Filesystem

The container filesystem is mounted read-only (read_only: true). A writable /tmp is provided as a tmpfs volume (128 MB in memory) for temporary files required by the JVM.

Secrets Management

Never hardcode secrets in docker-compose.yml. Use:

Environment file (.env):

bash
DB_PASSWORD=your-secure-database-password
JWT_SECRET=your-64-character-hex-secret

Docker secrets (Swarm mode):

yaml
secrets:
  db_password:
    external: true
  jwt_secret:
    external: true

Environment variable reference in docker-compose.yml:

yaml
environment:
  SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
  JWT_SECRET: ${JWT_SECRET}

Production Checklist

  • [ ] Generate a strong JWT_SECRET (64+ hex characters)
  • [ ] Use a unique, random DB_PASSWORD
  • [ ] Place behind a reverse proxy with TLS (Let's Encrypt)
  • [ ] Bind to 127.0.0.1 if proxy is local
  • [ ] Set up database backups (see Database)
  • [ ] Configure resource limits appropriate for your workload
  • [ ] Pin Docker image tag to a specific version (avoid latest in production)
  • [ ] Enable Docker log rotation to prevent disk exhaustion

Dockerfile Multi-Stage Build

The official image is built in three stages:

StageBase ImagePurpose
web-buildernode:24-alpineBuilds React 19 SPA with Vite
java-buildereclipse-temurin:25-jdk-alpineCompiles Spring Boot application with Gradle
runtimeeclipse-temurin:25-jre-nobleMinimal runtime with JRE only

The resulting image contains only the JRE and the pre-built static resources embedded in the server JAR — no JDK, no Node.js, no build toolchain.

Upgrading

bash
# 1. Update the image tag in docker-compose.yml
#    image: ghcr.io/mozhno-dev/mozhno:v1.1.0

# 2. Pull the new image and restart
docker compose pull mozhno
docker compose up -d mozhno

# 3. Flyway automatically applies new migrations on startup

The process is safe: the old container runs until the new one is ready. The health check ensures traffic is routed only after a successful start.

Rolling Back

bash
# Revert to the old tag in docker-compose.yml
docker compose pull mozhno
docker compose up -d mozhno

Flyway migrations are not automatically rolled back. If the new version added migrations, rolling back the code is safe (migrations are forward-compatible).

Reverse Proxy & TLS

In production, always place можно. behind a reverse proxy with HTTPS.

Nginx

nginx
server {
    listen 443 ssl http2;
    server_name flags.example.com;

    ssl_certificate     /etc/letsencrypt/live/flags.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/flags.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

In docker-compose.yml, bind the port to localhost only:

yaml
ports:
  - '127.0.0.1:8080:8080'

And set APP_BASE_URL to your domain:

yaml
APP_BASE_URL: https://flags.example.com

Caddy (Automatic TLS)

flags.example.com {
    reverse_proxy localhost:8080
}

Production Checklist

#ActionCommand / Variable
1Generate JWT secretopenssl rand -base64 32JWT_SECRET
2Strong database passwordSPRING_DATASOURCE_PASSWORD
3Set public domainAPP_BASE_URL=https://flags.example.com
4Configure CORSAPP_CORS_ALLOWED_ORIGINS=https://app.example.com
5Bind port to localhost onlyports: ['127.0.0.1:8080:8080']
6Set up TLS via Nginx/Caddy/TraefikSee section above
7Increase connection poolHIKARI_MAX_POOL_SIZE=30
8Configure SMTP for emailsSMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD
9Pin the image versionimage: ghcr.io/mozhno-dev/mozhno:v1.0.0
10Set up PostgreSQL backupspg_dump or WAL archiving, see Database
11Set up monitoringPrometheus, alerts — see Monitoring

Released under the AGPL v3.0 License.