Replacing EasyCron with self-hosted NexusCron
Replacing EasyCron with self-hosted NexusCron
Move scheduled HTTP jobs off EasyCron onto a scheduler you run. Job definitions, logs, and secrets stay on machines you control.
NexusCron license: https://nexuscore-store.whop.site
What you are replacing
EasyCron typically stores:
Job URL, HTTP method, headers, and body
Cron expression and timezone
Retry / timeout settings
Execution history in their dashboard
NexusCron runs the same shape of work as containers: a scheduler, a worker, Postgres for state, Redis for the queue.
Architecture
Internet / your apps
|
Caddy or Nginx (TLS)
|
nexuscron-api :8080
|
+----+----+
| |
Postgres Redis
| |
+----+----+
|
nexuscron-worker (N replicas)Prerequisites
Docker Engine 24+ and Docker Compose v2
PostgreSQL 15+ (or the bundled Postgres service)
A domain (
cron.example.com)1 vCPU / 1 GB RAM minimum (2 GB if you keep months of logs)
Outbound HTTPS from workers
EasyCron API token (read-only is enough to export)
1. Inventory EasyCron jobs
curl -sS "https://www.easycron.com/rest/list?token=$EASYCRON_TOKEN" \
| jq '.cron_jobs[] | {id, cron_job_name, cron_expression, url, http_method, timezone}'Keep the JSON dump. Map fields 1:1 into NexusCron.
2. Layout and secrets
mkdir -p /opt/nexuscron/{data/postgres,data/redis,config,backups}
cd /opt/nexuscron/opt/nexuscron/.env (never commit this):
NEXUSCRON_ENV=production
NEXUSCRON_LOG_LEVEL=info
NEXUSCRON_TZ=Europe/Madrid
NEXUSCRON_PUBLIC_URL=https://cron.example.com
NEXUSCRON_API_HOST=0.0.0.0
NEXUSCRON_API_PORT=8080
NEXUSCRON_API_TOKEN=replace-with-long-random-token
NEXUSCRON_JWT_SECRET=replace-with-64-char-random
POSTGRES_USER=nexuscron
POSTGRES_PASSWORD=replace-with-strong-password
POSTGRES_DB=nexuscron
DATABASE_URL=postgres://nexuscron:replace-with-strong-password@postgres:5432/nexuscron?sslmode=disable
REDIS_URL=redis://redis:6379/0
REDIS_PASSWORD=replace-with-redis-password
NEXUSCRON_WORKER_CONCURRENCY=8
NEXUSCRON_HTTP_TIMEOUT_SECONDS=60
NEXUSCRON_MAX_RETRIES=3
NEXUSCRON_RETRY_BACKOFF=exponential
NEXUSCRON_ALLOW_PRIVATE_TARGETS=false
NEXUSCRON_MAX_BODY_BYTES=1048576
NEXUSCRON_CORS_ORIGINS=https://cron.example.comopenssl rand -hex 323. Compose
Pin images to a digest in production. Bind the API to localhost. TLS terminates at Caddy or Nginx.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks: [internal]
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- ./data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks: [internal]
api:
image: ghcr.io/nexuscore/nexuscron-api:latest
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
networks: [internal, edge]
read_only: true
tmpfs: ["/tmp"]
security_opt:
- no-new-privileges:true
worker:
image: ghcr.io/nexuscore/nexuscron-worker:latest
restart: unless-stopped
env_file: .env
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
deploy:
replicas: 2
networks: [internal]
read_only: true
tmpfs: ["/tmp"]
security_opt:
- no-new-privileges:true
cap_drop: ["ALL"]
networks:
internal:
internal: true
edge: {}docker compose up -d
docker compose ps4. Caddy
cron.example.com {
encode gzip
reverse_proxy 127.0.0.1:8080
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy no-referrer
-Server
}
request_body {
max_size 1MB
}
}5. Map jobs and cut over
EasyCron | NexusCron |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
timezone |
|
timeout |
|
status |
|
curl -sS -X POST https://cron.example.com/v1/jobs \
-H "Authorization: Bearer $NEXUSCRON_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-webhook",
"cron_expr": "0 3 * * *",
"timezone": "Europe/Madrid",
"http_method": "POST",
"target_url": "https://app.example.com/hooks/nightly",
"headers": {"X-Cron-Secret": "replace-me"},
"timeout_seconds": 30,
"max_retries": 3,
"enabled": true
}'Cutover without double-firing:
Stack healthy, TLS valid,
/healthz200Pause jobs in EasyCron (do not delete)
Enable matching jobs in NexusCron
Watch one full cycle
Manually run anything due during the pause
Disable EasyCron jobs
Snapshot Postgres off-host
Set NEXUSCRON_ALLOW_PRIVATE_TARGETS=false so jobs cannot hit RFC1918 or metadata IPs.
docker compose exec -T postgres pg_dump -U nexcron nexcron | gzip > /opt/nexuscron/backups/nc-$(date +%F).sql.gzGet NexusCron
Lifetime license, source, Docker. Store: https://nexuscore-store.whop.site
Comment with the EasyCron job id, the NexusCron job id, and a redacted /health payload. Do not paste signing secrets.
