Simple HTTP proxy: squid

install.sh

#!/bin/sh

# Set timezone
timedatectl set-timezone America/Regina

# Update OS and install necessary packages
apt update
apt upgrade -y
apt install -y ca-certificates curl wget mc net-tools

# Install Docker GPG key
#curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
wget -O /etc/apt/keyrings/docker.asc https://download.docker.com/linux/ubuntu/gpg
chmod a+r /etc/apt/keyrings/docker.asc

# Install Docker repo
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

#install docker and co.
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin


# Create shell script for start/stop container
cat << EOF > /opt/squid/control_squid.sh
#!/bin/bash
case "\$1" in
    start)
        docker compose -f /opt/squid/docker-compose.yml up -d
        ;;
    stop)
        docker compose -f /opt/squid/docker-compose.yml down
        ;;
    restart)
        docker compose -f /opt/squid/docker-compose.yml down
        docker compose -f /opt/squid/docker-compose.yml up -d
        ;;
    *)
        echo "Usage: ./control_squid.sh {start|stop|restart}"
        exit 1
        ;;
esac
EOF
chmod +x /opt/squid/control_squid.sh

# Set up Docker for squid
cat << EOF > /opt/squid/docker-compose.yml
services:
  squid:
    image: ubuntu/squid:latest
    restart: unless-stopped
    ports:
      - "3128:3128"
    volumes:
      - ./logs:/var/log/squid
      - ./data:/var/spool/squid
      - ./conf:/etc/squid
    networks:
      inside:
        ipv4_address: 10.100.10.2
#    user: "1100:1100"
networks:
  inside:
    external: false
    ipam:
      config:
        - subnet: 10.100.10.0/24
EOF

# Create dirs
mkdir /opt/squid/logs
mkdir /opt/squid/conf
mkdir /opt/squid/data
chown -R 13:13 ./logs
chown -R 13:13 ./data

/opt/squid/conf/quid.conf content example

acl localnet src 0.0.0.1-0.255.255.255  # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8             # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10          # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16         # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12          # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16         # RFC 1918 local private network (LAN)
acl localnet src fc00::/7               # RFC 4193 local private network range
acl localnet src fe80::/10              # RFC 4291 link-local (directly plugged) machines
acl localnet src 142.3.31.0/24
acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 21          # ftp
acl Safe_ports port 443         # https
acl Safe_ports port 70          # gopher
acl Safe_ports port 210         # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280         # http-mgmt
acl Safe_ports port 488         # gss-http
acl Safe_ports port 591         # filemaker
acl Safe_ports port 777         # multiling http
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localhost
http_access allow localnet
http_access deny all
http_port 3128
coredump_dir /var/spool/squid
refresh_pattern ^ftp:           1440    20%     10080
refresh_pattern ^gopher:        1440    0%      1440
refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern .               0       20%     4320
logfile_rotate 0

cache_dir ufs /var/spool/squid 30000 16 256
cache_mem 512 MB
maximum_object_size 4 MB
cache_log /var/log/squid/cache.log

start container

/opt/squid/control_squid.sh start

done

Catch and forward traffic destined for specific host. Multiple destinations

This example for two destinations (could be more)

Install socat

apt install socat

Add iptables rules to firewalld (works only with IP – replace hostname with associated IP)

firewall-cmd --permanent --direct --add-rule ipv4 nat OUTPUT 0 -p tcp -d redcap.vumc.org --dport 443 -j REDIRECT --to-ports 12345
firewall-cmd --permanent --direct --add-rule ipv4 nat OUTPUT 0 -p tcp -d api.twilio.com --dport 443 -j REDIRECT --to-ports 12346
firewall-cmd --reload

check results

firewall-cmd --direct --get-all-rules

create systemd template

touch /etc/systemd/system/socat@.service

content of template

[Unit]
Description=Socat tunnel for port %i
After=network.target

[Service]
ExecStart=
ExecStart=/usr/bin/socat TCP4-LISTEN=%i,fork,reuseaddr PROXY:your_proxy.ca:example.com:443,proxyport=3128
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

We’ll override example.com per instance using systemctl edit.

systemctl edit socat@12345

content of socat@12345

[Service]
ExecStart=
ExecStart=/usr/bin/socat TCP4-LISTEN:12345,fork,reuseaddr PROXY:your_proxy.ca:redcap.vumc.org:443,proxyport=3128

another

systemctl edit socat@12346

content of socat@12346

[Service]
ExecStart=
ExecStart=/usr/bin/socat TCP4-LISTEN:12346,fork,reuseaddr PROXY:your_proxy.ca:api.twilio.com:443,proxyport=3128

start and enable both services

systemctl daemon-reload
systemctl enable --now socat@12345
systemctl enable --now socat@12346

check service status

systemctl status socat@12345
systemctl status socat@12346

done.

Little extra: Script example to update dynamically changed IP for hostnames

#!/bin/bash

LOGFILE="/var/log/redcap_firewall_update.log"
MIN_PORT=12345
MAX_PORT=12351
DPORT=443   # destination port to match on (e.g., HTTPS)

# List of "hostname redirect_port"
HOSTS_AND_PORTS=(
  "redcap.vumc.org 12345"
  "api.twilio.com 12346"
  "api.mosio.com 12347"
  "api.sendgrid.com 12348"
  "www.redcap-cats.org 12349"
  "redcap.link 12350"
  "cde.nlm.nih.gov 12351"
)

# Ensure the log file exists and is writable
touch "$LOGFILE" || { echo "Error: Cannot write to $LOGFILE"; exit 1; }

{
  echo "[$(date)] --- Starting firewall rule cleanup and insertion ---"
  echo ""

  # Get all permanent direct rules
  rules=$(firewall-cmd --permanent --direct --get-all-rules)

  # Loop through each rule and remove those within the port range
  while IFS= read -r rule; do
    to_port=$(echo "$rule" | grep -oP -- '--to-ports\s+\K[0-9]+')
    if [[ -n "$to_port" && "$to_port" -ge "$MIN_PORT" && "$to_port" -le "$MAX_PORT" ]]; then
      echo "Removing rule: $rule"
      firewall-cmd --permanent --direct --remove-rule $rule
    fi
  done <<< "$rules"

  echo "Reloading firewall after cleanup..."
  firewall-cmd --reload

  echo ""
  echo "Inserting updated rules..."

  # Add new rules
  for entry in "${HOSTS_AND_PORTS[@]}"; do
    HOSTNAME=$(echo "$entry" | awk '{print $1}')
    REDIRECT_PORT=$(echo "$entry" | awk '{print $2}')
    echo ""
    echo "Resolving $HOSTNAME..."
    echo ""
    IPV4S=$(dig +short "$HOSTNAME" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}')

    for IP in $IPV4S; do
      echo "Adding rule: $IP -> $REDIRECT_PORT"
      firewall-cmd --permanent --direct --add-rule ipv4 nat OUTPUT 0 -p tcp -d "$IP" --dport $DPORT -j REDIRECT --to-ports $REDIRECT_PORT
    done
  done

  echo "Reloading firewall after insertion..."
  firewall-cmd --reload

  echo ""
  echo "[$(date)] --- Firewall update complete ---"
  echo ""
} >> "$LOGFILE" 2>&1

Schedule it in crontab

0 */4 * * * /bin/bash /opt/update_redcap_firewall.sh

be happy!

Catch and forward traffic destined for specific host

Install socat

apt install socat

Create IPtables NAT rules

iptables -t nat -N REDCAP_SOCAT
iptables -t nat -A REDCAP_SOCAT -p tcp -d 160.129.8.154 --dport 443 -j REDIRECT --to-ports 12345
iptables -t nat -A OUTPUT -p tcp -d 160.129.8.154 --dport 443 -j REDCAP_SOCAT

Test socat in foregroud (160.129.8.154 – is IP of redcap.vumc.org)

socat TCP4-LISTEN:12345,fork,reuseaddr PROXY:your_proxy.ca:redcap.vumc.org:443,proxyport=3128

make it permanent/create service

touch /etc/systemd/system/socat-redcap-proxy.service

content of socat-redcap-proxy.service

[Unit]
Description=Socat proxy tunnel for redcap.vumc.org via your_proxy.ca:3128
After=network.target

[Service]
ExecStart=/usr/bin/socat TCP4-LISTEN:12345,fork,reuseaddr PROXY:your_proxy.ca:redcap.vumc.org:443,proxyport=3128
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Enable it

systemctl daemon-reexec
systemctl daemon-reload
systemctl enable --now socat-redcap-proxy

enjoy

Autorun script in 5 min after reboot

Create file.

touch /etc/systemd/system/delayed-script.service

Fill it.

[Unit]
Description=Run Script as Cognos User 5 Minutes After Boot
After=network.target

[Service]
Type=simple
User=user
Group=group
ExecStartPre=/bin/sleep 300
ExecStart=/opt/somescript.sh
Restart=no
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

enable it and reboot server

systemctl enable delayed-script.service
shutdown -r now

This will start script in 5 min after server reboot as “user” user

Zabbix proxy as Docker container

Goal is to have a zabbix proxy in docker container running under Ubuntu.

Solution: adjust following script to your needs. run it and get zabbix proxy in container. simple!

Script:

#!/bin/sh

# Update OS and install necessary packages
apt-get update
apt-get install -y ca-certificates curl wget

# Install Docker GPG key
wget -O /etc/apt/keyrings/docker.asc https://download.docker.com/linux/ubuntu/gpg
chmod a+r /etc/apt/keyrings/docker.asc

# Install Docker repo
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

# Set up Zabbix repository
wget https://repo.zabbix.com/zabbix/6.4/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.4-1+ubuntu24.04_all.deb
dpkg -i zabbix-release_6.4-1+ubuntu24.04_all.deb
apt update

# Install Zabbix agent and Docker engine
apt install -y zabbix-agent2 zabbix-agent2-plugin-* docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Configure Zabbix agent
cat << EOF > /etc/zabbix/zabbix_agent2.conf
PidFile=/var/run/zabbix/zabbix_agent2.pid
LogFile=/var/log/zabbix/zabbix_agent2.log
LogFileSize=0
Server=your_zabbix_server.ca
Hostname=Zabbix server
Include=/etc/zabbix/zabbix_agent2.d/*.conf
PluginSocket=/run/zabbix/agent.plugin.sock
ControlSocket=/run/zabbix/agent.sock
Include=/etc/zabbix/zabbix_agent2.d/plugins.d/*.conf
EOF

# Add user zabbix to Docker group
usermod -aG docker zabbix

# Restart Zabbix agent
systemctl restart zabbix-agent2


# Create shell script for start/stop container
cat << EOF > /opt/zabbix-proxy/restart_proxy.sh
#!/bin/sh
docker compose -f /opt/zabbix-proxy/docker-compose.yml down
docker compose -f /opt/zabbix-proxy/docker-compose.yml up -d
EOF
chmod +x /opt/zabbix-proxy/restart_proxy.sh

# Set up Docker for zabbix-proxy
cat << EOF > /opt/zabbix-proxy/docker-compose.yml
services:
  zabbix_proxy:
    image: zabbix/zabbix-proxy-sqlite3:ubuntu-6.0.12
    restart: always
    environment:
      - ZBX_SERVER_HOST=your_zabbix_server.ca
      - ZBX_HOSTNAME=zabbix_proxy_host.ca
      - ZBX_CONFIGFREQUENCY=60
    network_mode: host
EOF

# Start zabbix-proxy
docker compose -f /opt/zabbix-proxy/docker-compose.yml up -d

# Clean up
rm zabbix-release_6.4-1+ubuntu24.04_all.deb
rm install.sh
echo
echo ATTN!!! In zabbix-proxy2 config add primary/main interface of VM to "server" section and restart zabbix-agent2 service!

WordPress in docker deployment

Case: customer want to have WordPress in docker container.

Solution: below. πŸ™‚

Folder structure:

wp/
β”‚
β”œβ”€β”€ control_wp.sh
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ activate_server.sh
β”œβ”€β”€ renew.sh
β”œβ”€β”€ certbot_etc/
└── nginx_conf/
    └── nginx.conf
    └── nginx.template1
    └── nginx.template2
    └── options-ssl-nginx.conf
└── nginx_logs/
└── wp_db/
└── wp_html/

control script content (control_wp.sh)

#!/bin/bash
case "$1" in
    start)
        docker compose -f /opt/wp/docker-compose.yml up -d
        ;;
    stop)
        docker compose -f /opt/wp/docker-compose.yml down
        ;;
    restart)
        docker compose -f /opt/wp/docker-compose.yml down
        docker compose -f /opt/wp/docker-compose.yml up -d
        ;;
    *)
        echo "Usage: ./control_wp.sh {start|stop|restart}"
        exit 1
        ;;
esac

docker compose file

services:
  wordpress:
    image: wordpress:6.6.1-php8.1-fpm
    restart: unless-stopped
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp-user
      WORDPRESS_DB_PASSWORD: put_you_BD_password_here
      WORDPRESS_DB_NAME: wp-db
    volumes:
      - ./wp_html:/var/www/html
    networks:
      - inside
    depends_on:
      - db

  db:
    image: mariadb:10.6.18
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wp-db
      MYSQL_USER: wp-user
      MYSQL_PASSWORD: put_you_BD_password_here
      MYSQL_RANDOM_ROOT_PASSWORD: 'extra_difficult_password'
    volumes:
      - ./wp_db:/var/lib/mysql
    networks:
      - inside

  nginx-proxy:
    depends_on:
      - wordpress
    image: nginx:1.26.1-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./wp_html:/var/www/html
      - ./nginx_conf:/etc/nginx/conf.d
      - ./nginx_logs:/var/log/nginx
      - ./certbot_etc:/etc/letsencrypt
    networks:
      - inside

  certbot:
    depends_on:
      - nginx-proxy
    image: certbot/certbot
    volumes:
      - ./certbot_etc:/etc/letsencrypt
      - ./wp_html:/var/www/html
    command: certonly --webroot --webroot-path=/var/www/html --email user@domain.ca --agree-tos --no-eff-email -d your_domain.ca


networks:
  inside:
    external: false
    ipam:
      config:
        - subnet: 10.100.11.0/24

nginx_conf/nginx.conf example:

server {
        listen 80;

        server_name your_domain.ca;

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        server_name your_domain.ca;

        index index.php index.html index.htm;

        root /var/www/html;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/your_domain.ca/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your_domain.ca/privkey.pem;

        include /etc/nginx/conf.d/options-ssl-nginx.conf;

        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        # enable strict transport security only if you understand the implications

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass wordpress:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info;
        }

        location ~ /\.ht {
                deny all;
        }

        location = /favicon.ico {
                log_not_found off; access_log off;
        }
        location = /robots.txt {
                log_not_found off; access_log off; allow all;
        }
        location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
                expires max;
                log_not_found off;
        }
}

nginx_conf/nginx.template1 example:

server {
        listen 80;

        server_name your_domain.ca;

        index index.php index.html index.htm;

        root /var/www/html;

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass wordpress:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info;
        }

        location ~ /\.ht {
                deny all;
        }

        location = /favicon.ico {
                log_not_found off; access_log off;
        }
        location = /robots.txt {
                log_not_found off; access_log off; allow all;
        }
        location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
                expires max;
                log_not_found off;
        }
}

nginx_conf/nginx.template2 example:

server {
        listen 80;

        server_name your_domain.ca;

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        server_name your_domain.ca;

        index index.php index.html index.htm;

        root /var/www/html;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/your_domain.ca/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your_domain.ca/privkey.pem;

        include /etc/nginx/conf.d/options-ssl-nginx.conf;

        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        # enable strict transport security only if you understand the implications

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass wordpress:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info;
        }

        location ~ /\.ht {
                deny all;
        }

        location = /favicon.ico {
                log_not_found off; access_log off;
        }
        location = /robots.txt {
                log_not_found off; access_log off; allow all;
        }
        location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
                expires max;
                log_not_found off;
        }
}

search for “your_domain.ca” and replace with your values.

nginx_conf/options-ssl-nginx.conf example:

# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file. Contents are based on https://ssl-config.mozilla.org

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

activate_server.sh content:

# Start containers and obtain ssl certificate
cd /opt/wp
cp ./nginx_conf/nginx.template1 ./nginx_conf/nginx.conf
./control_wp.sh start
echo
echo Waiting for 60 sec to get new ssl cert
echo
sleep 60
cp ./nginx_conf/nginx.template2 ./nginx_conf/nginx.conf
./control_wp.sh restart
rm activate_server.sh

execute following script to finalize installation

/opt/wp/activate_server.sh

Go to https://your-server-domain-name and follow on-screen instaructions to configure wordpress server.

Enjoy clean wordpress installation.

use following script to renew SSL certificate (renew.sh).

#!/bin/bash

COMPOSE="/usr/bin/docker compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /opt/wp
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP nginx-proxy
$DOCKER system prune -af

Open Journal Systems (OJS) in Docker

Scenario: customer want to have OJS in docker containers.

Solution: official OJS docker container + Mariadb container deployed on Ubuntu 24.04

Solution. this solution specifically designed for OJS v3_3_0-8. upgrade from it to 3_3_0-9 working fine, but for different versions there could be some differences.

Update OS and install necesary packages

apt-get update
apt-get install -y ca-certificates curl wget

install docker GPG key

curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

Install docker repo

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

Install docker engine

apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

create folders and assign rights

mkdir /opt/ojs
cd /opt/ojs
mkdir ./volumes && chown 100:101 ./volumes -R
mkdir ./volumes/private && chown 100:101 ./volumes/private -R
mkdir ./volumes/public && chown 100:101 ./volumes/public -R
mkdir ./volumes/db && chown 999:999 ./volumes/db -R
mkdir ./volumes/logs && mkdir ./volumes/logs/db && chown 999:999 ./volumes/logs/db -R
mkdir ./volumes/config && wget "https://github.com/pkp/ojs/raw/3_3_0-8/config.TEMPLATE.inc.php" -O ./volumes/config/ojs.config.inc.php

create shell-script to run container.

cat << EOF > /opt/ojs/control_ojs.sh
#!/bin/bash
case "\$1" in
    start)
        docker compose -f /opt/ojs/docker-compose.yml up -d
        ;;
    stop)
        docker compose -f /opt/ojs/docker-compose.yml down
        ;;
    restart)
        docker compose -f /opt/ojs/docker-compose.yml down
        docker compose -f /opt/ojs/docker-compose.yml up -d
        ;;
    *)
        echo "Usage: ./control_ojs.sh {start|stop|restart}"
        exit 1
        ;;
esac
EOF
chmod +x /opt/ojs/control_ojs.sh

Create docker compose file

cat << EOF > /opt/ojs/docker-compose.yml
services:
  db:
    image: mariadb:10.2
    container_name: "ojs_db_demo"
    environment:
      MYSQL_ROOT_PASSWORD: "ojsPwd"
      MYSQL_DATABASE: "ojs"
      MYSQL_USER: "ojs"
      MYSQL_PASSWORD: "ojsPwd"
    volumes:
      - ./volumes/db:/var/lib/mysql
      - ./volumes/dump:/docker-entrypoint-initdb.d
    networks:
      - inside
    restart: always

  ojs:
    image: pkpofficial/ojs:3_3_0-8
    container_name: "ojs_app_demo"
    hostname: "demo"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime
      - ./volumes/private:/var/www/files
      - ./volumes/public:/var/www/html/public
      - ./volumes/logs/app:/var/log/apache2
#      # WARNING: You can only enable file-volumes if file exists in the container.
      - ./volumes/config/ojs.config.inc.php:/var/www/html/config.inc.php
    networks:
      - inside
    depends_on:
      - db
    restart: always

networks:
  inside:
    external: false
    ipam:
      config:
        - subnet: 10.100.10.0/24
EOF

start container

/opt/ojs/control_ojs.sh start

After initial configuration (or if you migrating DB from different server), edit file /opt/ojs/docker-compose.yml to comment out (or delete) following lines and restart container after that:

#    environment:
#      MYSQL_ROOT_PASSWORD: "ojsPwd"
#      MYSQL_DATABASE: "ojs"
#      MYSQL_USER: "ojs"
#      MYSQL_PASSWORD: "ojsPwd"

done.

Upgrade/update procedure for OJS

Edit fileΒ /opt/ojs/docker-compose.ymlΒ – correct line with desired version of OJS:Β image: pkpofficial/ojs:3_3_0-8

restart container:

/opt/ojs/control_ojs.sh restart

When containers started, execute following command and watch for errors:

docker exec -it ojs_app /usr/local/bin/ojs-upgrade

Wait for line similar to:Β Successfully upgraded to version 3.3.0.9

Note: Upgrading to different major/minor versions may require additional steps – probably changes to configuration file.

Note: Also, as upgrade path recommended to update to latest available build for minor version and update to first available build in next minor version. for example to upgrade 3.3.0.3->3.4.0.6, do following updates: 3.3.0.3->3.3.0.18, 3.3.0.18->3.4.0.1, 3.4.0.1->3.4.0.6

Happy journaling!

Custom-build Docker for Autodesk license server

Scenario: Customer want to have Licensing server in docker for simplify of update/upgrade process and simplify license update process.

Solution: Custom-build Docker container.

Solution.

Create dirs for project

mkdir /opt/lic && mkdir /opt/lic/files && mkdir /opt/lic/license && cd /opt/lic

Drop your license file to

/opt/lic/license/license.dat

Dockerfile to build container

FROM rockylinux:8.5

#########################################
##        BUILD-TIME VARIABLES        ##
#########################################
# url for Network Licence Manager
ARG NLM_URL=https://damassets.autodesk.net/content/dam/autodesk/www/files/linux/nlm11-19-4-1-ipv4-ipv6-linux64.tar.gz
# path for temporary files
ARG TEMP_PATH=/tmp/flexnetserver

#########################################
##        ENVIRONMENTAL CONFIG         ##
#########################################
# add the flexlm commands to $PATH
ENV PATH="$PATH:/opt/flexnetserver/"

#########################################
##         RUN INSTALL SCRIPT          ##
#########################################
COPY /files /usr/local/bin

RUN yum install -y redhat-lsb-core wget && yum clean all

## Set timezone to Regina
RUN yum install -y tzdata && yum clean all
RUN ln -sf /usr/share/zoneinfo/America/Regina /etc/localtime

WORKDIR $TEMP_PATH
RUN wget --progress=bar:force -- $NLM_URL
RUN tar -zxvf ./*.tar.gz
RUN rpm -vhi ./*.rpm
RUN rm -rf $TEMP_PATH
RUN mkdir /opt/logs

# lmadmin is required for -2 -p flag support
RUN groupadd -r lmadmin && \
    useradd -r -g lmadmin lmadmin

#########################################
##              VOLUMES                ##
#########################################
VOLUME ["/var/flexlm"]

#########################################
##            EXPOSE PORTS             ##
#########################################
EXPOSE 2079
EXPOSE 2080

# want to run it under lmadmin, not root. But it not working as expected - need to do more tests.
#USER lmadmin

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# no CMD, use container as if 'lmgrd'

content of docker-compose.yml

services:
  lic-server:
    image: autodesk-lic-server:latest
    restart: unless-stopped
    hostname: "lic.your-domain.ca"
    volumes:
      - ./license/license.dat:/usr/local/flexlm/licenses/license.dat
      - ./logs/:/opt/logs/
    ports:
      - "2079:2079"
      - "2080:2080"
    networks:
      inside:
        mac_address: 00:00:00:00:00:00
#Enter correct MAC for your license server

networks:
  inside:
    external: false
    ipam:
      config:
        - subnet: 10.100.10.0/24

Content of control script (/opt/lic/control_lic.sh)

#!/bin/bash
case "$1" in
    start)
        docker compose -f /opt/lic/docker-compose.yml up -d
        ;;
    stop)
        docker compose -f /opt/lic/docker-compose.yml down
        ;;
    restart)
        docker compose -f /opt/lic/docker-compose.yml down
        docker compose -f /opt/lic/docker-compose.yml up -d
        ;;
    *)
        echo "Usage: ./control_lic.sh {start|stop|restart}"
        exit 1
        ;;
esac

content of files/entypoint.sh

#!/bin/bash
set -e

echo ""

# to help users determine their LMHostID
lmutil lmhostid

echo ""
echo "look at ./logs/log.log for more details"
echo ""

# forward all command line arguments to lmgrd
# NOTE: lmgrd -z flag is required to 'Run in foreground.' so that
#       Docker will not start sleeping regardless flags.
lmgrd -z >> /opt/logs/log.log 2>&1

build container with

docker build --tag autodesk-lic-server .

start server with

/opt/lic/control_lic.sh start

done.

How to update license.

Replace license file with new one here: /opt/lic/license/license.dat.

Restart container with:

/opt/lic/control_lic.sh restart

done

how to upgrade license server.

Locate official page to download new license server release. Example:Β https://www.autodesk.com/support/technical/article/caas/tsarticles/ts/2HEQiQ5PtCDDk6WX0ox1mq.html

Copy direct link to new license server release. Example:Β https://damassets.autodesk.net/content/dam/autodesk/www/files/linux/nlm11-19-4-1-ipv4-ipv6-linux64.tar.gz

Edit Dockerfile and replace link to new license server (ARG NLM_URL=).

Rebuild container with:

docker build --tag autodesk-lic-server .

Watch for errors in output.

Restart container with:

/opt/lic/control_lic.sh restart

done

Custom-build Docker for Matlab Licensing server

Scenario: Customer want to have Licensing server in docker for simplify of update/upgrade process and simplify license update process.

Solution: Custom-build Docker container.

Solution.

Create dirs for project

mkdir /opt/lic_matlab && mkdir /opt/lic_matlab/license && mkdir /opt/lic_matlab/files && cd /opt/lic_matlab

Replace license file with correct one:Β /opt/lic_matlab/license/license.datΒ 

Create shell-script to control docker and other files:

touch /opt/lic_matlab/control_lic.sh
touch /opt/lic_matlab/docker-commpose.yml
touch /opt/lic_matlab/Dockerfile
chmod +x /opt/lic_matlab/control_lic.sh

content of control_lic.sh

#!/bin/bash
case "$1" in
    start)
        docker compose -f /opt/lic_matlab/docker-compose.yml up -d
        ;;
    stop)
        docker compose -f /opt/lic_matlab/docker-compose.yml down
        ;;
    restart)
        docker compose -f /opt/lic_matlab/docker-compose.yml down
        docker compose -f /opt/lic_matlab/docker-compose.yml up -d
        ;;
    *)
        echo "Usage: ./control_lic.sh {start|stop|restart}"
        exit 1
        ;;
esac

Content of docker-compose.yml

services:
  lic-server:
    image: matlab-lic-server:latest
    restart: unless-stopped
    hostname: "lic.your-domain.ca"
    volumes:
      - ./license/license.dat:/etc/lmgrd/licenses/license.dat
      - ./var/log/lic_matlab/:/opt/logs/
    ports:
      - "9010:9010"
      - "9011:9011"
    networks:
      inside:
        mac_address: 00:00:00:00:00:00
# replace MAC with correct one

networks:
  inside:
    external: false
    ipam:
      config:
        - subnet: 10.100.10.0/24

content of Dockerfile

FROM rockylinux:8.5

# add the flexlm commands to /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/lmgrd/etc/glnxa64"

COPY /files /usr/local/bin

RUN yum update -y && yum install wget unzip -y && mkdir /lmgrd \
    && cd /lmgrd && wget https://ssd.mathworks.com/supportfiles/downloads/R2024b/license_manager/R2024b/daemons/glnxa64/mathworks_network_license_manager_glnxa64.zip \
    && unzip mathworks_network_license_manager_glnxa64.zip \
    && rm -vf mathworks_network_license_manager_glnxa64.zip \
    && mkdir /opt/logs && mkdir /etc/lmgrd && mkdir /etc/lmgrd/licenses

## Set timezone to Regina
RUN yum install -y tzdata && yum clean all
RUN ln -sf /usr/share/zoneinfo/America/Regina /etc/localtime

VOLUME /etc/lmgrd/licenses
VOLUME /usr/tmp
ENV LM_LICENSE_FILE=/etc/lmgrd/licenses/license.dat
ENV LMGRD_PORT=9010
ENV MLM_PORT=9011
EXPOSE $LMGRD_PORT
EXPOSE $MLM_PORT

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

content of files/entrypoint.sh

#!/bin/bash
set -e

echo ""

# to help users determine their LMHostID
lmutil lmhostid

echo ""
echo "look at ./logs/log.log for more details"
echo ""

# forward all command line arguments to lmgrd
# NOTE: lmgrd -z flag is required to 'Run in foreground.' so that
#       Docker will not start sleeping regardless flags.
lmgrd -z >> /opt/logs/log.log 2>&1

Build container with:

docker build --tag matlab-lic-server .

Start container with:

/opt/lic_matlab/control_lic.sh start

License update porocess:

1. backup.
2. Replace license file with new one here: /opt/lic_matlab/license/license.dat
3. restart container with: /opt/lic_matlab/control_lic.sh restart

update/upgrade license server:

Locate official page to download new license server release. Example:Β https://www.mathworks.com/support/install/license_manager_files.html?s_tid=srchtitle_site_search_2_download%20license%20server

Copy direct link to new license server release. Example:Β https://ssd.mathworks.com/supportfiles/downloads/R2024b/license_manager/R2024b/daemons/glnxa64/mathworks_network_license_manager_glnxa64.zip

Edit Dockerfile and replace link to new license server (locate command wget and replace argument with new link).

Rebuild container with:

docker build --tag matlab-lic-server .

Watch for errors in output.

Restart container with:

/opt/lic_matlab/control_lic.sh restart

done.

Full LAMP stack in docker

Scenario: Customer want to have entire LAMP stack in docker for better scalability, simple management, updates, etc.

Solution: This will install standard LAMP stack + phpMyAdmin + HTTPS with Let`s encrypt. (this specific configuration created for OJS 3.5. Modify it for your needs)

Solution directory structure:

lamp-docker/
β”‚
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ letsecrypt/
β”‚   └── acme.json
└── php/
    └── Dockerfile
    └── vhost.conf

Docker-compose content:

networks:
  lampnet:
    driver: bridge

services:
  traefik:
    image: traefik:3.6
    container_name: traefik
    restart: always
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@your-domain.ca"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--log.level=INFO"
      - "--accesslog=true"
      - "--accesslog.filepath=/var/log/traefik/access.log"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
      - ./logs/traefik:/var/log
    networks:
      - lampnet

  apache-php:
    build: ./php
    container_name: apache-php
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.ojs.rule=Host(`your-domain.ca`)"
      - "traefik.http.routers.ojs.entrypoints=websecure"
      - "traefik.http.routers.ojs.tls.certresolver=letsencrypt"
      - "traefik.http.services.ojs.loadbalancer.server.port=80"
      - "traefik.http.middlewares.ojs-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.middlewares.ojs-headers.headers.customrequestheaders.X-Forwarded-Host=your-domain.ca"
      - "traefik.http.routers.ojs.middlewares=ojs-headers"
    volumes:
      - ./www:/var/www/html
      - ./logs/apache:/var/log/apache2
    networks:
      - lampnet

  mariadb:
    image: mariadb:11
    container_name: mariadb
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_superpassword
      MYSQL_DATABASE: ojsdb
      MYSQL_USER: ojsuser
      MYSQL_PASSWORD: superpassword
    command: --bind-address=0.0.0.0
    ports:
      - "127.0.0.1:3306:3306"
    volumes:
      - ./mariadb_data:/var/lib/mysql
    networks:
      - lampnet

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: phpmyadmin
    restart: always
    environment:
      PMA_HOST: mariadb
      PMA_PORT: 3306
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pma.rule=Host(`your-domain.ca`) && PathPrefix(`/myadmin`)"
      - "traefik.http.routers.pma.entrypoints=websecure"
      - "traefik.http.routers.pma.tls.certresolver=letsencrypt"
      - "traefik.http.middlewares.pma-add-slash.redirectregex.regex=^(.*/myadmin)$"
      - "traefik.http.middlewares.pma-add-slash.redirectregex.replacement=$1/"
      - "traefik.http.middlewares.pma-add-slash.redirectregex.permanent=true"
      - "traefik.http.middlewares.pma-stripprefix.stripprefix.prefixes=/myadmin"
      - "traefik.http.routers.pma.middlewares=pma-add-slash,pma-stripprefix"
      - "traefik.http.services.pma.loadbalancer.server.port=80"
    volumes:
      - ./logs/phpmyadmin:/var/log
    networks:
      - lampnet

Content of php/Dockerfile:

FROM php:8.3-apache

RUN apt-get update && apt-get install -y \
    libicu-dev \
    libxml2-dev \
    libzip-dev \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libonig-dev \
    libbz2-dev \
    unzip \
    ftp \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install intl mbstring xml bcmath zip gd ftp pdo_mysql mysqli\
    && a2enmod rewrite \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*


# Virtual host
COPY vhost.conf /etc/apache2/sites-available/000-default.conf
COPY custom-php.ini /usr/local/etc/php/conf.d/

# Set Apache log location to host-mounted volume
RUN mkdir -p /var/log/apache2
RUN chown -R www-data:www-data /var/www/html

content of php/vhost.conf (Apache virtual host).

<VirtualHost *:80>
    ServerName your-domain.ca
    DocumentRoot /var/www/html
    ErrorLog /var/log/apache2/your-domain-error.log
    CustomLog /var/log/apache2/your-domain-access.log combined

    <Directory /var/www/html>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

content of php/custom-php.ini

memory_limit = 256M
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 120

how to connect to DB from host:

install mysql-client on host. 
use following connand to connect:
mysql --protocol=TCP -h 127.0.0.1 -P 3306 -u ojsuser -p

Script for operations. control.sh

#!/bin/bash

set -e

COMMAND=$1

show_help() {
    echo "Usage: $0 {start|stop|build|restart|rebuild|status}"
    echo
    echo "  start     - Start all containers"
    echo "  stop      - Stop all containers"
    echo "  build     - Build local container images"
    echo "  restart   - Stop then start containers"
    echo "  rebuild   - Stop β†’ rebuild images β†’ start"
    echo "  status    - Show running service list"
    exit 1
}

if [ -z "$COMMAND" ]; then
    show_help
fi

case "$COMMAND" in

    start)
        echo "---- Starting containers ----"
        docker compose up -d
        echo "---- Done ----"
        ;;

    stop)
        echo "---- Stopping containers ----"
        docker compose down
        echo "---- Done ----"
        ;;

    build)
        echo "---- Building container images ----"
        docker compose build
        echo "---- Done ----"
        ;;

    restart)
        echo "---- Restarting containers ----"
        docker compose down
        docker compose up -d
        echo "---- Done ----"
        ;;

    rebuild)
        echo "---- Stopping containers ----"
        docker compose down
        echo "---- Rebuilding images ----"
        docker compose build --no-cache
        echo "---- Starting containers ----"
        docker compose up -d
        echo "---- Done ----"
        ;;

    status)
        echo "---- Container status ----"
        docker compose ps
        ;;

    *)
        echo "Error: Unknown command '$COMMAND'"
        show_help
        ;;
esac

Start containers with script. Build process depends on your system and internet connection. In my case it took around 5 min to build containers and start services.

Done.