Compare commits

...

11 Commits

Author SHA1 Message Date
Rostislav Dugin
5f3c4f23d7 FIX (dependencies): Run extra dependencies via go mod tidy 2025-07-21 21:21:44 +03:00
Rostislav Dugin
ecb8212eab FEATURE (gin): Add griz compression for static files and API responses 2025-07-21 21:19:27 +03:00
Rostislav Dugin
0e178343a8 FIX (monitoring): Fix text of down and up messages to not be the same in heading and body 2025-07-21 20:59:20 +03:00
Rostislav Dugin
0acd205f43 FIX (restores): Fix order of temp files closing that causes flaky tests 2025-07-21 20:01:19 +03:00
Rostislav Dugin
d678f9b3a2 FEATURE (container): Move PostgreSQL into container 2025-07-21 19:36:42 +03:00
Rostislav Dugin
7859951653 Merge branch 'main' of https://github.com/RostislavDugin/postgresus 2025-07-21 14:59:12 +03:00
Rostislav Dugin
7472aa1e1f FIX (backups): Do not double close backup file 2025-07-21 14:58:34 +03:00
Rostislav Dugin
9283713eab Merge pull request #6 from RostislavDugin/feature/update_readme
FEATURE (readme): Move badges under the description [skip-release]
2025-07-21 14:47:39 +03:00
Rostislav Dugin
9a9c170ffc FEATURE (readme): Move badges under the description [skip-release] 2025-07-21 14:43:48 +03:00
Rostislav Dugin
d05efc3151 FIX (deployments): Remove Docker Hub description update 2025-07-21 14:13:50 +03:00
Rostislav Dugin
1ee41fb673 FEATURE (auth): Add rate limiting for sign in edpoint to not allow brute force 2025-07-21 14:00:31 +03:00
20 changed files with 164 additions and 133 deletions

View File

@@ -336,16 +336,6 @@ jobs:
rostislavdugin/postgresus:v${{ needs.determine-version.outputs.new_version }}
rostislavdugin/postgresus:${{ github.sha }}
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v4
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: rostislavdugin/postgresus
short-description: "Free PostgreSQL monitoring & backup solution with multi-storage support"
readme-filepath: ./README.md
release:
runs-on: ubuntu-latest
needs: [determine-version, build-and-push]

View File

@@ -53,18 +53,23 @@ RUN CGO_ENABLED=0 \
# ========= RUNTIME =========
FROM --platform=$TARGETPLATFORM debian:bookworm-slim
# Install PostgreSQL client tools (versions 13-17)
# Install PostgreSQL server and client tools (versions 13-17)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget ca-certificates gnupg lsb-release && \
wget ca-certificates gnupg lsb-release sudo gosu && \
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-17 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-client-16 postgresql-client-17 && \
rm -rf /var/lib/apt/lists/*
# Create postgres user and set up directories
RUN useradd -m -s /bin/bash postgres || true && \
mkdir -p /postgresus-data/pgdata && \
chown -R postgres:postgres /postgresus-data/pgdata
WORKDIR /app
# Copy Goose from build stage
@@ -87,7 +92,71 @@ RUN if [ ! -f /app/.env ]; then \
fi; \
fi
# Create startup script
COPY <<EOF /app/start.sh
#!/bin/bash
set -e
# PostgreSQL 17 binary paths
PG_BIN="/usr/lib/postgresql/17/bin"
# Ensure proper ownership of data directory
echo "Setting up data directory permissions..."
mkdir -p /postgresus-data/pgdata
chown -R postgres:postgres /postgresus-data
# Initialize PostgreSQL if not already initialized
if [ ! -s "/postgresus-data/pgdata/PG_VERSION" ]; then
echo "Initializing PostgreSQL database..."
gosu postgres \$PG_BIN/initdb -D /postgresus-data/pgdata --encoding=UTF8 --locale=C.UTF-8
# Configure PostgreSQL
echo "host all all 127.0.0.1/32 md5" >> /postgresus-data/pgdata/pg_hba.conf
echo "local all all trust" >> /postgresus-data/pgdata/pg_hba.conf
echo "port = 5437" >> /postgresus-data/pgdata/postgresql.conf
echo "listen_addresses = 'localhost'" >> /postgresus-data/pgdata/postgresql.conf
echo "shared_buffers = 256MB" >> /postgresus-data/pgdata/postgresql.conf
echo "max_connections = 100" >> /postgresus-data/pgdata/postgresql.conf
fi
# Start PostgreSQL in background
echo "Starting PostgreSQL..."
gosu postgres \$PG_BIN/postgres -D /postgresus-data/pgdata -p 5437 &
POSTGRES_PID=\$!
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
if gosu postgres \$PG_BIN/pg_isready -p 5437 -h localhost >/dev/null 2>&1; then
echo "PostgreSQL is ready!"
break
fi
if [ \$i -eq 30 ]; then
echo "PostgreSQL failed to start"
exit 1
fi
sleep 1
done
# Create database and set password for postgres user
echo "Setting up database and user..."
gosu postgres \$PG_BIN/psql -p 5437 -h localhost -d postgres << 'SQL'
ALTER USER postgres WITH PASSWORD 'Q1234567';
CREATE DATABASE "postgresus" OWNER postgres;
\q
SQL
# Start the main application
echo "Starting Postgresus application..."
exec ./main
EOF
RUN chmod +x /app/start.sh
EXPOSE 4005
ENTRYPOINT ["./main"]
# Volume for PostgreSQL data
VOLUME ["/postgresus-data"]
ENTRYPOINT ["/app/start.sh"]
CMD []

View File

@@ -1,5 +1,8 @@
<div align="center">
<img src="assets/logo.svg" alt="Postgresus Logo" width="250"/>
<img src="assets/logo.svg" style="margin-bottom: 20px;" alt="Postgresus Logo" width="250"/>
<h3>PostgreSQL monitoring and backup</h3>
<p>Free, open source and self-hosted solution for automated PostgreSQL monitoring and backups. With multiple storage options and notifications</p>
<!-- Badges -->
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -9,9 +12,6 @@
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-13%20%7C%2014%20%7C%2015%20%7C%2016%20%7C%2017-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/RostislavDugin/postgresus)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/RostislavDugin/postgresus)
<h3>PostgreSQL monitoring and backup</h3>
<p>Free, open source and self-hosted solution for automated PostgreSQL monitoring and backups. With multiple storage options and notifications</p>
<p>
<a href="#-features">Features</a> •
@@ -64,21 +64,29 @@
- **Historical data**: View trends and patterns over time
- **Alert system**: Get notified when issues are detected
### 📦 Installation
You have three ways to install Postgresus:
- Script (recommended)
- Simple Docker run
- Docker Compose setup
<img src="assets/healthchecks.svg" alt="Postgresus Dashboard" width="800"/>
---
## 📦 Installation
You have two ways to install Postgresus: via automated script (recommended) or manual Docker Compose setup.
You have three ways to install Postgresus: automated script (recommended), simple Docker run, or Docker Compose setup.
### Option 1: Automated Installation Script (Recommended, Linux only)
The installation script will:
- ✅ Install Docker with Docker Compose (if not already installed)
-Create optimized `docker-compose.yml` configuration
-Set up automatic startup on system reboot via cron
- ✅ Install Docker with Docker Compose(if not already installed)
-Set up Postgresus
-Configure automatic startup on system reboot
```bash
sudo apt-get install -y curl && \
@@ -86,7 +94,26 @@ sudo curl -sSL https://raw.githubusercontent.com/RostislavDugin/postgresus/refs/
| sudo bash
```
### Option 2: Manual Docker Compose Setup
### Option 2: Simple Docker Run
The easiest way to run Postgresus with embedded PostgreSQL:
```bash
docker run -d \
--name postgresus \
-p 4005:4005 \
-v ./postgresus-data:/postgresus-data \
--restart unless-stopped \
rostislavdugin/postgresus:latest
```
This single command will:
- ✅ Start Postgresus
- ✅ Store all data in `./postgresus-data` directory
- ✅ Automatically restart on system reboot
### Option 3: Docker Compose Setup
Create a `docker-compose.yml` file with the following configuration:
@@ -101,29 +128,6 @@ services:
- "4005:4005"
volumes:
- ./postgresus-data:/postgresus-data
depends_on:
postgresus-db:
condition: service_healthy
restart: unless-stopped
postgresus-db:
container_name: postgresus-db
image: postgres:17
# we use default values, but do not expose
# PostgreSQL ports so it is safe
environment:
- POSTGRES_DB=postgresus
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=Q1234567
volumes:
- ./pgdata:/var/lib/postgresql/data
command: -p 5437
shm_size: 10gb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgresus -p 5437"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
```

View File

@@ -5,9 +5,9 @@ DEV_DB_PASSWORD=Q1234567
#app
ENV_MODE=production
# db
DATABASE_DSN=host=postgresus-db user=postgres password=Q1234567 dbname=postgresus port=5437 sslmode=disable
DATABASE_URL=postgres://postgres:Q1234567@postgresus-db:5437/postgresus?sslmode=disable
DATABASE_DSN=host=localhost user=postgres password=Q1234567 dbname=postgresus port=5437 sslmode=disable
DATABASE_URL=postgres://postgres:Q1234567@localhost:5437/postgresus?sslmode=disable
# migrations
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://postgres:Q1234567@postgresus-db:5437/postgresus?sslmode=disable
GOOSE_DBSTRING=postgres://postgres:Q1234567@localhost:5437/postgresus?sslmode=disable
GOOSE_MIGRATION_DIR=./migrations

View File

@@ -1,15 +0,0 @@
repos:
- repo: local
hooks:
- id: golangci-lint-fmt
name: Format Go Code using golangci-lint fmt
entry: golangci-lint fmt ./...
language: system
types: [go]
- id: golangci-lint-run
name: Run golangci-lint for static analysis
entry: golangci-lint run
language: system
types: [go]
pass_filenames: false

View File

@@ -31,6 +31,7 @@ import (
_ "postgresus-backend/swagger" // swagger docs
"github.com/gin-contrib/cors"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
@@ -61,6 +62,15 @@ func main() {
gin.SetMode(gin.ReleaseMode)
ginApp := gin.Default()
// Add GZIP compression middleware
ginApp.Use(gzip.Gzip(
gzip.DefaultCompression,
// Don't compress already compressed files
gzip.WithExcludedExtensions(
[]string{".png", ".gif", ".jpeg", ".jpg", ".ico", ".svg", ".pdf", ".mp4"},
),
))
enableCors(ginApp)
setUpRoutes(ginApp)
setUpDependencies()

View File

@@ -4,6 +4,7 @@ go 1.23.3
require (
github.com/gin-contrib/cors v1.7.5
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
@@ -19,6 +20,7 @@ require (
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.39.0
golang.org/x/time v0.12.0
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.26.1
)

View File

@@ -37,8 +37,8 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -252,6 +252,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -255,10 +255,10 @@ func (uc *CreatePostgresqlBackupUsecase) streamToStorage(
copyResultCh <- err
}()
// Wait for the dump and copy to finish
waitErr := cmd.Wait()
// Wait for the copy to finish first, then the dump process
copyErr := <-copyResultCh
bytesWritten := <-bytesWrittenCh
waitErr := cmd.Wait()
// Check for shutdown before finalizing
if config.IsShouldShutdown() {

View File

@@ -224,7 +224,7 @@ func (uc *CheckPgHealthUseCase) sendDbStatusNotification(
messageBody := ""
if newHealthStatus == databases.HealthStatusAvailable {
messageTitle = fmt.Sprintf("✅ [%s] DB is back online", database.Name)
messageTitle = fmt.Sprintf("✅ [%s] DB is online", database.Name)
messageBody = fmt.Sprintf("✅ [%s] DB is back online", database.Name)
} else {
messageTitle = fmt.Sprintf("❌ [%s] DB is unavailable", database.Name)

View File

@@ -303,7 +303,7 @@ func Test_CheckPgHealthUseCase(t *testing.T) {
t,
"SendNotification",
mock.Anything,
fmt.Sprintf("✅ [%s] DB is back online", database.Name),
fmt.Sprintf("✅ [%s] DB is online", database.Name),
fmt.Sprintf("✅ [%s] DB is back online", database.Name),
)
})

View File

@@ -222,11 +222,8 @@ func (uc *RestorePostgresqlBackupUsecase) downloadBackupToTempFile(
return "", nil, fmt.Errorf("failed to write backup to temporary file: %w", err)
}
// Close the temp file to ensure all data is written
if err := tempFile.Close(); err != nil {
cleanupFunc()
return "", nil, fmt.Errorf("failed to close temporary backup file: %w", err)
}
// Close the temp file to ensure all data is written - this is handled by defer
// Removing explicit close to avoid double-close error
uc.logger.Info("Backup file written to temporary location", "tempFile", tempBackupFile)
return tempBackupFile, cleanupFunc, nil

View File

@@ -62,8 +62,8 @@ func (l *LocalStorage) SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.R
return fmt.Errorf("failed to sync temp file: %w", err)
}
err = tempFile.Close()
if err != nil {
// Close the temp file explicitly before moving it (required on Windows)
if err = tempFile.Close(); err != nil {
logger.Error("Failed to close temp file", "fileId", fileID.String(), "error", err)
return fmt.Errorf("failed to close temp file: %w", err)
}

View File

@@ -222,10 +222,14 @@ func verifyDataIntegrity(t *testing.T, originalDB *sqlx.DB, restoredDB *sqlx.DB)
assert.NoError(t, err)
assert.Equal(t, len(originalData), len(restoredData), "Should have same number of rows")
for i := range originalData {
assert.Equal(t, originalData[i].ID, restoredData[i].ID, "ID should match")
assert.Equal(t, originalData[i].Name, restoredData[i].Name, "Name should match")
assert.Equal(t, originalData[i].Value, restoredData[i].Value, "Value should match")
// Only compare data if both slices have elements (to avoid panic)
if len(originalData) > 0 && len(restoredData) > 0 {
for i := range originalData {
assert.Equal(t, originalData[i].ID, restoredData[i].ID, "ID should match")
assert.Equal(t, originalData[i].Name, restoredData[i].Name, "Name should match")
assert.Equal(t, originalData[i].Value, restoredData[i].Value, "Value should match")
}
}
}

View File

@@ -4,10 +4,12 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
type UserController struct {
userService *UserService
userService *UserService
signinLimiter *rate.Limiter
}
func (c *UserController) RegisterRoutes(router *gin.RouterGroup) {
@@ -51,8 +53,18 @@ func (c *UserController) SignUp(ctx *gin.Context) {
// @Param request body SignInRequest true "User signin data"
// @Success 200 {object} SignInResponse
// @Failure 400
// @Failure 429 {object} map[string]string "Rate limit exceeded"
// @Router /users/signin [post]
func (c *UserController) SignIn(ctx *gin.Context) {
// We use rate limiter to prevent brute force attacks
if !c.signinLimiter.Allow() {
ctx.JSON(
http.StatusTooManyRequests,
gin.H{"error": "Rate limit exceeded. Please try again later."},
)
return
}
var request SignInRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})

View File

@@ -2,6 +2,8 @@ package users
import (
user_repositories "postgresus-backend/internal/features/users/repositories"
"golang.org/x/time/rate"
)
var secretKeyRepository = &user_repositories.SecretKeyRepository{}
@@ -12,6 +14,7 @@ var userService = &UserService{
}
var userController = &UserController{
userService,
rate.NewLimiter(rate.Limit(3), 3), // 3 RPS with burst of 3
}
func GetUserService() *UserService {

View File

@@ -86,15 +86,14 @@ Notifications flow:
Extra:
- add brute force protection on auth (via local RPS limiter) (in progress by Rostislav Dugin)
- create pretty website like rybbit.io with demo
- add HTTPS for Postgresus
- add simple SQL queries via UI
- add support of Kubernetes Helm
- create pretty website like rybbit.io with demo
Monitoring flow:
- add system metrics (CPU, RAM, disk, IO)
- add system metrics (CPU, RAM, disk, IO) (in progress by Rostislav Dugin)
- add queries stats (slowest, most frequent, etc. via pg_stat_statements)
- add alerting for slow queries (listen for slow query and if they reach >100ms - send message)
- add alerting for high resource usage (listen for high resource usage and if they reach >90% - send message)

View File

@@ -16,27 +16,4 @@ services:
volumes:
- ./postgresus-data:/postgresus-data
container_name: postgresus-local
depends_on:
postgresus-db:
condition: service_healthy
restart: unless-stopped
postgresus-db:
image: postgres:17
# we use default values, but do not expose
# PostgreSQL ports so it is safe
environment:
- POSTGRES_DB=postgresus
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=Q1234567
volumes:
- ./pgdata:/var/lib/postgresql/data
container_name: postgresus-db
command: -p 5437
shm_size: 10gb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgresus -p 5437"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
restart: unless-stopped

View File

@@ -341,7 +341,7 @@ export const EditBackupConfigComponent = ({
<Tooltip
className="cursor-pointer"
title="Number of CPU cores to use for backup processing. Higher values may speed up backups but use more resources."
title="Number of CPU cores to use for restore processing. Higher values may speed up restores, but use more resources."
>
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>

View File

@@ -68,29 +68,6 @@ services:
- "4005:4005"
volumes:
- ./postgresus-data:/postgresus-data
depends_on:
postgresus-db:
condition: service_healthy
restart: unless-stopped
postgresus-db:
container_name: postgresus-db
image: postgres:17
# we use default values, but do not expose
# PostgreSQL ports so it is safe
environment:
- POSTGRES_DB=postgresus
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=Q1234567
volumes:
- ./pgdata:/var/lib/postgresql/data
command: -p 5437
shm_size: 10gb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgresus -p 5437"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
EOF
log "docker-compose.yml created successfully"