Docker & Deployment
Heavily AI Assisted
Radish can run entirely in Docker — no Julia installation needed. This makes it easy to demo, test, and deploy.
Quick Start
# Build the image
docker compose build
# Start the server
docker compose up
# Connect a client (separate terminal)
docker compose run --rm radish-client
That’s it. The server starts on port 9000 and the client connects automatically.
Architecture
The Docker setup uses a single image for both server and client, with Docker Compose orchestrating the roles:
graph LR
subgraph Docker Network
S["radish-server<br/>Port 9000"]
C1["radish-client 1"]
C2["radish-client 2"]
end
V["radish-data<br/>(named volume)"]
C1 -->|TCP| S
C2 -->|TCP| S
S -->|persistence| V
Dockerfile
The Dockerfile uses Julia 1.11 as the base image and installs dependencies:
FROM julia:1.11
WORKDIR /app
COPY . .
RUN julia --project=. -e 'using Pkg; Pkg.instantiate()'
Docker Compose
services:
radish-server:
build: .
command: julia --project=. server_runner.jl 0.0.0.0 9000
ports:
- "9000:9000"
volumes:
- radish-data:/app/persistence
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9000"]
interval: 10s
radish-client:
build: .
command: julia --project=. client_runner.jl radish-server 9000
stdin_open: true
tty: true
volumes:
radish-data:
Key Design Decisions
Health Checks
The health check uses nc -z (netcat zero-I/O mode) — a lightweight TCP probe that just checks if the port is open:
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9000"]
interval: 10s
timeout: 3s
retries: 3
This avoids sending actual RESP commands for health checks. The server handles these connection-and-immediate-disconnect probes gracefully — the ECONNRESET from health check probes is caught and logged silently.
Configuration
The server reads its configuration from radish.yml at startup. Inside Docker, the config file is baked into the image during the build. To use a custom config, you can mount it as a volume:
volumes:
- ./my-config.yml:/app/radish.yml
Note that when running in Docker, the network.host should be 0.0.0.0 (not 127.0.0.1) to accept connections from other containers.
Data Persistence
Data is stored in a Docker named volume (radish-data), which means:
- Data survives
docker compose down→docker compose up - Data is isolated from any local Radish instance running outside Docker
- To wipe the database:
docker compose down -v(the-vflag removes volumes)
The .dockerignore
The .dockerignore file keeps the image lean:
.git
persistence/
Manifest.toml
persistence/ is excluded because the container uses its own volume. Manifest.toml is regenerated during the build by Pkg.instantiate().
Common Operations
All day-to-day operations are wrapped in make targets. Run make help to see the full list at any time.
Build
| Command | Description |
|---|---|
make build | Build the Docker image |
make rebuild | Force rebuild from scratch (no cache) |
Server
| Command | Description |
|---|---|
make server | Start the server in the background |
make server-logs | Tail the server logs (Ctrl+C to stop) |
make server-stop | Stop the server |
Client
| Command | Description |
|---|---|
make client | Attach an interactive client to the running server |
Simulator
| Command | Description |
|---|---|
make simulator | Run the workload simulator (load + run) |
make simload | Run simulator in load-only mode |
make simrun | Run simulator in run-only mode |
Docs
| Command | Description |
|---|---|
make docs-build | Build the docs Docker image |
make docs | Start the Jekyll docs server at http://localhost:4000 |
make docs-bg | Start the docs server in the background |
make docs-logs | Tail the docs logs (Ctrl+C to stop) |
make docs-stop | Stop the docs server |
Teardown & Utilities
| Command | Description |
|---|---|
make down | Stop and remove all containers |
make clean | Remove containers, networks and volumes (wipes persisted data) |
make ps | Show status of all Radish containers |
make logs | Tail logs for all running containers |
make help | Show all available commands |
Connecting from the Host
The server port is exposed on localhost:9000, so you can also connect from outside Docker:
# Using a local Julia client
julia client_runner.jl 127.0.0.1 9000
# Or any TCP tool
nc localhost 9000
Workload Simulator
The workload simulator (workload_simulator.jl) generates realistic client traffic against a running Radish server. It exercises every supported command across all data types, making it useful for stress testing, performance profiling, and validating correctness under load.
Modes
| Mode | Description |
|---|---|
load | Preload a fixed number of keys into the DB, then exit |
run | Execute random operations against existing keys |
loadrun | Load keys first, then run operations |
Docker Usage
make simulator # load + run (default)
make simload # load only
make simrun # run only
With custom parameters:
# Small quick test
docker compose --profile simulator run --rm radish-simulator \
julia --project=. workload_simulator.jl loadrun \
--host radish-server --port 9000 \
--num-clients 3 --num-keys 100 --num-ops 200
# Duration-based run (60 seconds)
docker compose --profile simulator run --rm radish-simulator \
julia --project=. workload_simulator.jl run \
--host radish-server --port 9000 \
--duration 60 --num-clients 20
# Continuous load (Ctrl+C to stop)
docker compose --profile simulator run --rm radish-simulator \
julia --project=. workload_simulator.jl run \
--host radish-server --port 9000 \
--forever --num-clients 50
Local Usage (no Docker)
julia workload_simulator.jl loadrun --num-keys 1000 --num-ops 5000
julia workload_simulator.jl load --num-clients 5 --num-keys 10000
julia workload_simulator.jl run --duration 30 --num-clients 10
CLI Options
| Option | Default | Description |
|---|---|---|
--num-clients N | 10 | Concurrent client connections |
--num-keys N | 5000 | Keys per type in load phase |
--num-ops N | 10000 | Operations per client in run phase |
--duration T | — | Run for T seconds instead of num-ops |
--forever | — | Run indefinitely (Ctrl+C to stop) |
--host HOST | 127.0.0.1 | Server host |
--port PORT | 9000 | Server port |
Load Phase
Creates --num-keys keys per supported type (currently: strings and lists):
- TTL: 50% of keys get a TTL between 10 min–1 hour; 50% are persistent
- Content size: Random between 5–500 (chars for strings, elements for lists)
- Integer strings: ~30% of string keys store integer values (for
S_INCRand friends) - Key naming:
str_1,str_2, … for strings;list_1,list_2, … for lists
Run Phase
Discovers existing keys via KLIST, then executes random operations with weighted probability:
| Category | Probability | Description |
|---|---|---|
| Type-specific ops | ~75% | All string and list commands, weighted by key count |
| Transactions | ~10% | MULTI → 3–8 same-type ops → EXEC (20% DISCARD) |
| Meta ops | ~10% | EXISTS, TYPE, TTL, PERSIST, EXPIRE, RENAME, DEL |
| General ops | ~5% | PING, DBSIZE, KLIST |
Expired keys are automatically removed from the pool after 2 consecutive nils, with a full pool refresh every 5,000 operations. FLUSHDB is excluded.
Adding a New Type
The simulator uses a Type Registry — adding a new Radish type requires only one entry:
const TYPE_REGISTRY = [
(name = "string", prefix = "str", create_fn = ..., ops = ..., tx_ops = ...),
(name = "list", prefix = "list", create_fn = ..., ops = ..., tx_ops = ...),
# (name = "hash", prefix = "hash", create_fn = ..., ops = ..., tx_ops = ...),
]
The load and run phases iterate over this registry automatically.