Building a Self-Hosted Image Hosting Service with Nginx on Ubuntu

Setting Up Nginx for Image Storage on Ubuntu

Installing Nginx

sudo apt update && sudo apt install nginx

Creating the Storage Directory

sudo mkdir -p /var/www/images/blog
sudo chmod -R 777 /var/www/images/blog

Nginx Configuration

Edit the configuration file at /etc/nginx/sites-available/default:

server {
    listen 80;
    server_name your-domain.com;

    location /images/ {
        root /var/www;
        autoindex on;
        expires 30d;
    }
}

Apply the changes:

sudo systemctl restart nginx

Security Enhancements

HTTPS Implementation: Obtain a free SSL certificate through Let's Encrypt.

File Type Restrictions:

if ($request_filename ~* \.(php|jsp)) { deny all; }

Hotlink Protection:

valid_referers blocked server_names;
if ($invalid_referer) { return 403; }

Spring Boot Backend Implementation

Upload Controller

@RestController
@RequestMapping("/api/upload")
public class ImageController {

    @Value("${file.storage-path}")
    private String storagePath;

    @PostMapping("/image")
    public ResponseEntity<Map<String, String>> handleImageUpload(
            @RequestParam("file") MultipartFile file) {

        String originalFilename = file.getOriginalFilename();
        String extension = FilenameUtils.getExtension(originalFilename);
        String generatedName = UUID.randomUUID() + "." + extension;

        Path destination = Paths.get(storagePath).resolve(generatedName);
        try {
            Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new RuntimeException("Failed to save image");
        }

        String imageUrl = "https://your-domain.com/images/blog/" + generatedName;
        return ResponseEntity.ok(Collections.singletonMap("url", imageUrl));
    }
}

Application Configuration

In application.yml:

file:
  storage-path: /var/www/images/blog

CORS Configuration

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:8080")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*");
    }
}

Vue Front end Implementation

Image Upload Component

<template>
  <el-upload
    action="#"
    :http-request="handleUpload"
    :show-file-list="false"
    :before-upload="validateFile">
    <el-button type="primary">Upload Image</el-button>
  </el-upload>
</template>

<script setup>
import axios from 'axios';

const handleUpload = async ({ file }) => {
    const formData = new FormData();
    formData.append('file', file);

    try {
        const response = await axios.post('/api/upload/image', formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
        });
        // Insert the returned URL into the rich text editor
        richEditor.insertEmbed(
            richEditor.getSelection().index,
            'image',
            response.data.url
        );
    } catch (error) {
        console.error('Upload failed', error);
    }
};

const validateFile = (file) => {
    const allowedTypes = ['image/jpeg', 'image/png'];
    const maxSize = 5 * 1024 * 1024;

    if (!allowedTypes.includes(file.type)) {
        ElMessage.error('Only JPG/PNG formats are supported');
        return false;
    }
    if (file.size > maxSize) {
        ElMessage.error('File size cannot exceed 5MB');
        return false;
    }
    return true;
};
</script>

Rich Text Editor Integration

const richEditor = new Quill('#editor', {
    modules: {
        toolbar: [
            ['bold', 'italic'],
            ['image']
        ]
    }
});

richEditor.getModule('toolbar').addHandler('image', () => {
    document.querySelector('.el-upload input').click();
});

Database Schema

Blog Table Structure

CREATE TABLE blog_posts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    content LONGTEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Entity Class

@Entity
@Table(name = "blog_posts")
public class BlogPost {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @Lob
    private String content;

    @CreationTimestamp
    private LocalDateTime createdAt;
}

Tags: Ubuntu nginx Image Hosting Spring Boot Vue.js

Posted on Wed, 14 Oct 2026 16:48:27 +0000 by krabople